├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── README.md ├── bin └── plugin_root ├── etc └── nodenv.d │ ├── version-name │ └── package-json-engine.bash │ └── version-origin │ └── package-json-engine.bash ├── libexec ├── JSON.sh ├── nodenv-package-json-engine └── semver.sh ├── node_modules ├── JSON.sh │ └── JSON.sh └── sh-semver │ └── semver.sh ├── package-lock.json ├── package.json └── test ├── fixtures ├── node-x.y.z │ └── bin │ │ └── node └── nodenv_root │ ├── plugins │ └── nodenv-package-json-engine │ └── versions │ ├── 4.0.0 │ ├── 4.2.1 │ └── 5.0.0 ├── nodenv-package-json-engine.bats └── test_helper.bash /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: { push: { tags: 'v[0-9]+.[0-9]+.[0-9]+' } } 3 | 4 | jobs: 5 | github: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: { fetch-depth: 0 } 10 | - run: npm run -s relnotes | tee relnotes.txt 11 | - uses: jasonkarns/create-release@master 12 | with: { body_path: relnotes.txt } 13 | 14 | homebrew: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: mislav/bump-homebrew-formula-action@v1 18 | with: { formula-name: nodenv } 19 | env: 20 | COMMITTER_TOKEN: ${{ secrets.BOT_TOKEN }} 21 | 22 | npm: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | scope: nodenv 29 | registry-url: https://registry.npmjs.org 30 | - run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v2 10 | - run: npm cit 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /nodenv-nodenv-package-json-engine-*.tgz 3 | /yarn.lock 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodenv-package-json-engine 2 | 3 | [![Latest GitHub Release](https://img.shields.io/github/v/release/nodenv/nodenv-package-json-engine?logo=github&sort=semver)](https://github.com/nodenv/nodenv-package-json-engine/releases/latest) 4 | [![Latest npm Release](https://img.shields.io/npm/v/@nodenv/nodenv-package-json-engine)](https://www.npmjs.com/package/@nodenv/nodenv-package-json-engine/v/latest) 5 | [![Test](https://img.shields.io/github/workflow/status/nodenv/nodenv-package-json-engine/Test?label=tests&logo=github)](https://github.com/nodenv/nodenv-package-json-engine/actions?query=workflow%3ATest) 6 | 7 | This is a plugin for [nodenv](https://github.com/nodenv/nodenv) 8 | that detects the Node version based on the [`engines`](https://docs.npmjs.com/files/package.json#engines) field of the current tree's `package.json` file. The `$NODENV_VERSION` environment variable (set with `nodenv shell`) and `.node-version` files still take precedence. 9 | 10 | When `engines` is configured with a range this plugin chooses the greatest installed version matching the range, or exits with an error if none match. 11 | 12 | 13 | 14 | - [Installation](#installation) 15 | * [Installing with Git](#installing-with-git) 16 | * [Installing with Homebrew](#installing-with-homebrew) 17 | - [Usage](#usage) 18 | - [Contributing](#contributing) 19 | - [Credits](#credits) 20 | 21 | 22 | 23 | ## Installation 24 | 25 | ### Installing with Git 26 | 27 | ```sh 28 | $ git clone https://github.com/nodenv/nodenv-package-json-engine.git $(nodenv root)/plugins/nodenv-package-json-engine 29 | ``` 30 | 31 | ### Installing with Homebrew 32 | 33 | Mac OS X users can install [many nodenv plugins](https://github.com/nodenv/homebrew-nodenv) with [Homebrew](http://brew.sh). 34 | 35 | *This is the recommended method of installation if you installed nodenv with 36 | Homebrew.* 37 | 38 | ```sh 39 | $ brew tap nodenv/nodenv 40 | $ brew install nodenv-package-json-engine 41 | ``` 42 | 43 | ## Usage 44 | 45 | Once you've installed the plugin you can verify that it's working by `cd`ing into a project that has a `package.json` file with `engines` and does not have a `.node-version` file. From anywhere in the project's tree, run `nodenv which node`. 46 | 47 | ## Contributing 48 | 49 | `npm install` and `npm test` from within the project. 50 | 51 | ## Credits 52 | 53 | `package.json` inspection and SemVer integration heavily inspired by nvmish [[1]](https://github.com/goodeggs/homebrew-delivery-eng/blob/master/nvmish.sh) [[2]](https://gist.github.com/assaf/ee377a186371e2e269a7) and [rbenv-bundler-ruby-version](https://github.com/aripollak/rbenv-bundler-ruby-version). 54 | 55 | Shell semver range support provided by [sh-semver](https://github.com/qzb/sh-semver). 56 | 57 | `package.json` parsing provided by [JSON.sh](https://github.com/dominictarr/JSON.sh). 58 | -------------------------------------------------------------------------------- /bin/plugin_root: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Writes the full path to the root of this plugin to standard out. 4 | # Use to source or run files in libexec/ 5 | # 6 | # Adapted from: 7 | # http://www.ostricher.com/2014/10/the-right-way-to-get-the-directory-of-a-bash-script/ 8 | # http://stackoverflow.com/a/12694189/407845 9 | src="${BASH_SOURCE[0]}" 10 | 11 | # while $src is a symlink, resolve it 12 | while [ -h "$src" ]; do 13 | dir="${src%/*}" 14 | src="$( readlink "$src" )" 15 | 16 | # If $src was a relative symlink (so no "/" as prefix), 17 | # need to resolve it relative to the symlink base directory 18 | [[ $src != /* ]] && src="$dir/$src" 19 | done 20 | dir="${src%/*}" 21 | 22 | if [ -d "$dir" ]; then 23 | echo "$dir/.." 24 | else 25 | echo "$PWD/.." 26 | fi 27 | -------------------------------------------------------------------------------- /etc/nodenv.d/version-name/package-json-engine.bash: -------------------------------------------------------------------------------- 1 | # shellcheck source=libexec/nodenv-package-json-engine 2 | source "$(plugin_root)/libexec/nodenv-package-json-engine" 3 | 4 | if ! NODENV_PACKAGE_JSON_VERSION=$(get_version_respecting_precedence); then 5 | echo "package-json-engine: version satisfying \`$(get_expression_respecting_precedence)' is not installed" >&2 6 | exit 1 7 | elif [ -n "$NODENV_PACKAGE_JSON_VERSION" ]; then 8 | # shellcheck disable=2034 9 | NODENV_VERSION="${NODENV_PACKAGE_JSON_VERSION}" 10 | fi 11 | unset NODENV_PACKAGE_JSON_VERSION 12 | -------------------------------------------------------------------------------- /etc/nodenv.d/version-origin/package-json-engine.bash: -------------------------------------------------------------------------------- 1 | # shellcheck source=libexec/nodenv-package-json-engine 2 | source "$(plugin_root)/libexec/nodenv-package-json-engine" 3 | 4 | ENGINES_EXPRESSION=$(get_expression_respecting_precedence); 5 | if [ -n "$ENGINES_EXPRESSION" ]; then 6 | # shellcheck disable=2034 7 | NODENV_VERSION_ORIGIN="$(package_json_path)#engines.node" 8 | fi 9 | unset ENGINES_EXPRESSION 10 | -------------------------------------------------------------------------------- /libexec/JSON.sh: -------------------------------------------------------------------------------- 1 | ../node_modules/JSON.sh/JSON.sh -------------------------------------------------------------------------------- /libexec/nodenv-package-json-engine: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # If a custom Node version is not already defined, we look 4 | # for a Node version semver expressing in the current tree's package.json. 5 | # If we find a fixed version, we print it out. If we find a range we 6 | # test the installed versions against that range and print the 7 | # greatest matching version. 8 | 9 | # Vendored scripts: 10 | JSON_SH="$(plugin_root)/node_modules/JSON.sh/JSON.sh" 11 | SEMVER="$(plugin_root)/node_modules/sh-semver/semver.sh" 12 | 13 | # Exits non-zero if this plugin should yield precedence 14 | # Gives precedence to local and shell versions. 15 | # Takes precedence over global version. 16 | package_json_has_precedence() { 17 | [ -z "$(nodenv local 2>/dev/null)" ] && 18 | [ -z "$(nodenv sh-shell 2>/dev/null)" ] 19 | } 20 | 21 | find_package_json_path() { 22 | local package_json root="${1-}" 23 | while [ -n "$root" ]; do 24 | package_json="$root/package.json" 25 | 26 | if [ -r "$package_json" ] && [ -f "$package_json" ]; then 27 | echo "$package_json" 28 | return 29 | fi 30 | root="${root%/*}" 31 | done 32 | } 33 | 34 | extract_version_from_package_json() { 35 | package_json_path="${1-}" 36 | version_regex='\["engines","node"\][[:space:]]*"([^"]*)"' 37 | # -b -n gives minimal output - see https://github.com/dominictarr/JSON.sh#options 38 | [[ $("$JSON_SH" -b -n < "$package_json_path" 2>/dev/null) =~ $version_regex ]] 39 | echo "${BASH_REMATCH[1]-}" 40 | } 41 | 42 | find_installed_version_matching_expression() { 43 | version_expression="${1-}" 44 | local -a installed_versions 45 | while IFS= read -r v; do 46 | installed_versions+=( "$v" ) 47 | done < <(nodenv versions --bare --skip-aliases | grep -e '^[[:digit:]]') 48 | 49 | local fast_guess 50 | fast_guess=$("$SEMVER" -r "$version_expression" "${installed_versions[@]:${#installed_versions[@]}-1}" | tail -n 1) 51 | 52 | # Most #engine version specs just specify a baseline version, 53 | # which means most likely, the highest installed version will satisfy 54 | # This does a first pass with just that single version in hopes it satisfies. 55 | # If so, we can avoid the cost of sh-semver sorting and validating across 56 | # all the installed versions. 57 | if [ -n "$fast_guess" ]; then 58 | echo "$fast_guess" 59 | return 60 | fi 61 | 62 | "$SEMVER" -r "$version_expression" "${installed_versions[@]}" | tail -n 1 63 | } 64 | 65 | package_json_path() { 66 | path=$(find_package_json_path "${PWD-}") 67 | [ -r "$path" ] && [ -f "$path" ] && echo "$path" 68 | } 69 | 70 | get_version_respecting_precedence() { 71 | if ! package_json_has_precedence; then return; fi 72 | 73 | package_json_path=$(package_json_path) || return 0 74 | 75 | version_expression=$( 76 | extract_version_from_package_json "$package_json_path" 77 | ) 78 | if [ -z "$version_expression" ]; then return; fi 79 | 80 | version=$( 81 | find_installed_version_matching_expression "$version_expression" 82 | ) 83 | if [ -z "$version" ]; then return 1; fi 84 | echo "$version" 85 | } 86 | 87 | get_expression_respecting_precedence() { 88 | if ! package_json_has_precedence; then return; fi 89 | 90 | package_json_path=$(package_json_path) || return 0 91 | 92 | version_expression=$( 93 | extract_version_from_package_json "$package_json_path" 94 | ) 95 | echo "$version_expression" 96 | } 97 | -------------------------------------------------------------------------------- /libexec/semver.sh: -------------------------------------------------------------------------------- 1 | ../node_modules/sh-semver/semver.sh -------------------------------------------------------------------------------- /node_modules/JSON.sh/JSON.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | throw() { 4 | echo "$*" >&2 5 | exit 1 6 | } 7 | 8 | BRIEF=0 9 | LEAFONLY=0 10 | PRUNE=0 11 | NO_HEAD=0 12 | NORMALIZE_SOLIDUS=0 13 | 14 | usage() { 15 | echo 16 | echo "Usage: JSON.sh [-b] [-l] [-p] [-s] [-h]" 17 | echo 18 | echo "-p - Prune empty. Exclude fields with empty values." 19 | echo "-l - Leaf only. Only show leaf nodes, which stops data duplication." 20 | echo "-b - Brief. Combines 'Leaf only' and 'Prune empty' options." 21 | echo "-n - No-head. Do not show nodes that have no path (lines that start with [])." 22 | echo "-s - Remove escaping of the solidus symbol (straight slash)." 23 | echo "-h - This help text." 24 | echo 25 | } 26 | 27 | parse_options() { 28 | set -- "$@" 29 | local ARGN=$# 30 | while [ "$ARGN" -ne 0 ] 31 | do 32 | case $1 in 33 | -h) usage 34 | exit 0 35 | ;; 36 | -b) BRIEF=1 37 | LEAFONLY=1 38 | PRUNE=1 39 | ;; 40 | -l) LEAFONLY=1 41 | ;; 42 | -p) PRUNE=1 43 | ;; 44 | -n) NO_HEAD=1 45 | ;; 46 | -s) NORMALIZE_SOLIDUS=1 47 | ;; 48 | ?*) echo "ERROR: Unknown option." 49 | usage 50 | exit 0 51 | ;; 52 | esac 53 | shift 1 54 | ARGN=$((ARGN-1)) 55 | done 56 | } 57 | 58 | awk_egrep () { 59 | local pattern_string=$1 60 | 61 | gawk '{ 62 | while ($0) { 63 | start=match($0, pattern); 64 | token=substr($0, start, RLENGTH); 65 | print token; 66 | $0=substr($0, start+RLENGTH); 67 | } 68 | }' pattern="$pattern_string" 69 | } 70 | 71 | tokenize () { 72 | local GREP 73 | local ESCAPE 74 | local CHAR 75 | 76 | if echo "test string" | egrep -ao --color=never "test" >/dev/null 2>&1 77 | then 78 | GREP='egrep -ao --color=never' 79 | else 80 | GREP='egrep -ao' 81 | fi 82 | 83 | if echo "test string" | egrep -o "test" >/dev/null 2>&1 84 | then 85 | ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' 86 | CHAR='[^[:cntrl:]"\\]' 87 | else 88 | GREP=awk_egrep 89 | ESCAPE='(\\\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' 90 | CHAR='[^[:cntrl:]"\\\\]' 91 | fi 92 | 93 | local STRING="\"$CHAR*($ESCAPE$CHAR*)*\"" 94 | local NUMBER='-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][+-]?[0-9]*)?' 95 | local KEYWORD='null|false|true' 96 | local SPACE='[[:space:]]+' 97 | 98 | # Force zsh to expand $A into multiple words 99 | local is_wordsplit_disabled=$(unsetopt 2>/dev/null | grep -c '^shwordsplit$') 100 | if [ $is_wordsplit_disabled != 0 ]; then setopt shwordsplit; fi 101 | $GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | egrep -v "^$SPACE$" 102 | if [ $is_wordsplit_disabled != 0 ]; then unsetopt shwordsplit; fi 103 | } 104 | 105 | parse_array () { 106 | local index=0 107 | local ary='' 108 | read -r token 109 | case "$token" in 110 | ']') ;; 111 | *) 112 | while : 113 | do 114 | parse_value "$1" "$index" 115 | index=$((index+1)) 116 | ary="$ary""$value" 117 | read -r token 118 | case "$token" in 119 | ']') break ;; 120 | ',') ary="$ary," ;; 121 | *) throw "EXPECTED , or ] GOT ${token:-EOF}" ;; 122 | esac 123 | read -r token 124 | done 125 | ;; 126 | esac 127 | [ "$BRIEF" -eq 0 ] && value=$(printf '[%s]' "$ary") || value= 128 | : 129 | } 130 | 131 | parse_object () { 132 | local key 133 | local obj='' 134 | read -r token 135 | case "$token" in 136 | '}') ;; 137 | *) 138 | while : 139 | do 140 | case "$token" in 141 | '"'*'"') key=$token ;; 142 | *) throw "EXPECTED string GOT ${token:-EOF}" ;; 143 | esac 144 | read -r token 145 | case "$token" in 146 | ':') ;; 147 | *) throw "EXPECTED : GOT ${token:-EOF}" ;; 148 | esac 149 | read -r token 150 | parse_value "$1" "$key" 151 | obj="$obj$key:$value" 152 | read -r token 153 | case "$token" in 154 | '}') break ;; 155 | ',') obj="$obj," ;; 156 | *) throw "EXPECTED , or } GOT ${token:-EOF}" ;; 157 | esac 158 | read -r token 159 | done 160 | ;; 161 | esac 162 | [ "$BRIEF" -eq 0 ] && value=$(printf '{%s}' "$obj") || value= 163 | : 164 | } 165 | 166 | parse_value () { 167 | local jpath="${1:+$1,}$2" isleaf=0 isempty=0 print=0 168 | case "$token" in 169 | '{') parse_object "$jpath" ;; 170 | '[') parse_array "$jpath" ;; 171 | # At this point, the only valid single-character tokens are digits. 172 | ''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;; 173 | *) value=$token 174 | # if asked, replace solidus ("\/") in json strings with normalized value: "/" 175 | [ "$NORMALIZE_SOLIDUS" -eq 1 ] && value=$(echo "$value" | sed 's#\\/#/#g') 176 | isleaf=1 177 | [ "$value" = '""' ] && isempty=1 178 | ;; 179 | esac 180 | [ "$value" = '' ] && return 181 | [ "$NO_HEAD" -eq 1 ] && [ -z "$jpath" ] && return 182 | 183 | [ "$LEAFONLY" -eq 0 ] && [ "$PRUNE" -eq 0 ] && print=1 184 | [ "$LEAFONLY" -eq 1 ] && [ "$isleaf" -eq 1 ] && [ $PRUNE -eq 0 ] && print=1 185 | [ "$LEAFONLY" -eq 0 ] && [ "$PRUNE" -eq 1 ] && [ "$isempty" -eq 0 ] && print=1 186 | [ "$LEAFONLY" -eq 1 ] && [ "$isleaf" -eq 1 ] && \ 187 | [ $PRUNE -eq 1 ] && [ $isempty -eq 0 ] && print=1 188 | [ "$print" -eq 1 ] && printf "[%s]\t%s\n" "$jpath" "$value" 189 | : 190 | } 191 | 192 | parse () { 193 | read -r token 194 | parse_value 195 | read -r token 196 | case "$token" in 197 | '') ;; 198 | *) throw "EXPECTED EOF GOT $token" ;; 199 | esac 200 | } 201 | 202 | if ([ "$0" = "$BASH_SOURCE" ] || ! [ -n "$BASH_SOURCE" ]); 203 | then 204 | parse_options "$@" 205 | tokenize | parse 206 | fi 207 | 208 | # vi: expandtab sw=2 ts=2 209 | -------------------------------------------------------------------------------- /node_modules/sh-semver/semver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | _num_part='([0-9]|[1-9][0-9]*)' 4 | _lab_part='([0-9]|[1-9][0-9]*|[0-9]*[a-zA-Z-][a-zA-Z0-9-]*)' 5 | _met_part='([0-9A-Za-z-]+)' 6 | 7 | RE_NUM="$_num_part(\.$_num_part)*" 8 | RE_LAB="$_lab_part(\.$_lab_part)*" 9 | RE_MET="$_met_part(\.$_met_part)*" 10 | RE_VER="[ \t]*$RE_NUM(-$RE_LAB)?(\+$RE_MET)?" 11 | 12 | BRE_DIGIT='[0-9]\{1,\}' 13 | BRE_ALNUM='[0-9a-zA-Z-]\{1,\}' 14 | BRE_IDENT="$BRE_ALNUM\(\.$BRE_ALNUM\)*" 15 | 16 | BRE_MAJOR="$BRE_DIGIT" 17 | BRE_MINOR="\(\.$BRE_DIGIT\)\{0,1\}" 18 | BRE_PATCH="\(\.$BRE_DIGIT\)\{0,1\}" 19 | BRE_PRERE="\(-$BRE_IDENT\)\{0,1\}" 20 | BRE_BUILD="\(+$BRE_IDENT\)\{0,1\}" 21 | BRE_VERSION="${BRE_MAJOR}${BRE_MINOR}${BRE_PATCH}${BRE_PRERE}${BRE_BUILD}" 22 | 23 | filter() 24 | { 25 | local text="$1" 26 | local regex="$2" 27 | shift 2 28 | echo "$text" | grep -E "$@" "$regex" 29 | } 30 | 31 | # Gets number part from normalized version 32 | get_number() 33 | { 34 | echo "${1%%-*}" 35 | } 36 | 37 | # Gets prerelase part from normalized version 38 | get_prerelease() 39 | { 40 | local pre_and_meta=${1%+*} 41 | local pre=${pre_and_meta#*-} 42 | if [ "$pre" = "$1" ]; then 43 | echo 44 | else 45 | echo "$pre" 46 | fi 47 | } 48 | 49 | # Gets major number from normalized version 50 | get_major() 51 | { 52 | echo "${1%%.*}" 53 | } 54 | 55 | # Gets minor number from normalized version 56 | get_minor() 57 | { 58 | local minor_major_bug=${1%%-*} 59 | local minor_major=${minor_major_bug%.*} 60 | local minor=${minor_major#*.} 61 | 62 | if [ "$minor" = "$minor_major" ]; then 63 | echo 64 | else 65 | echo "$minor" 66 | fi 67 | } 68 | 69 | get_bugfix() 70 | { 71 | local minor_major_bug=${1%%-*} 72 | local bugfix=${minor_major_bug##*.*.} 73 | 74 | if [ "$bugfix" = "$minor_major_bug" ]; then 75 | echo 76 | else 77 | echo "$bugfix" 78 | fi 79 | } 80 | 81 | strip_metadata() 82 | { 83 | echo "${1%+*}" 84 | } 85 | 86 | semver_eq() 87 | { 88 | local ver1 ver2 part1 part2 89 | ver1=$(get_number "$1") 90 | ver2=$(get_number "$2") 91 | 92 | local count=1 93 | while true; do 94 | part1=$(echo "$ver1"'.' | cut -d '.' -f $count) 95 | part2=$(echo "$ver2"'.' | cut -d '.' -f $count) 96 | 97 | if [ -z "$part1" ] || [ -z "$part2" ]; then 98 | break 99 | fi 100 | 101 | if [ "$part1" != "$part2" ]; then 102 | return 1 103 | fi 104 | 105 | local count=$(( count + 1 )) 106 | done 107 | 108 | if [ "$(get_prerelease "$1")" = "$(get_prerelease "$2")" ]; then 109 | return 0 110 | else 111 | return 1 112 | fi 113 | } 114 | 115 | semver_lt() 116 | { 117 | local number_a number_b prerelease_a prerelease_b 118 | number_a=$(get_number "$1") 119 | number_b=$(get_number "$2") 120 | prerelease_a=$(get_prerelease "$1") 121 | prerelease_b=$(get_prerelease "$2") 122 | 123 | 124 | local head_a='' 125 | local head_b='' 126 | local rest_a=$number_a. 127 | local rest_b=$number_b. 128 | while [ -n "$rest_a" ] || [ -n "$rest_b" ]; do 129 | head_a=${rest_a%%.*} 130 | head_b=${rest_b%%.*} 131 | rest_a=${rest_a#*.} 132 | rest_b=${rest_b#*.} 133 | 134 | if [ -z "$head_a" ] || [ -z "$head_b" ]; then 135 | return 1 136 | fi 137 | 138 | if [ "$head_a" -eq "$head_b" ]; then 139 | continue 140 | fi 141 | 142 | if [ "$head_a" -lt "$head_b" ]; then 143 | return 0 144 | else 145 | return 1 146 | fi 147 | done 148 | 149 | if [ -n "$prerelease_a" ] && [ -z "$prerelease_b" ]; then 150 | return 0 151 | elif [ -z "$prerelease_a" ] && [ -n "$prerelease_b" ]; then 152 | return 1 153 | fi 154 | 155 | local head_a='' 156 | local head_b='' 157 | local rest_a=$prerelease_a. 158 | local rest_b=$prerelease_b. 159 | while [ -n "$rest_a" ] || [ -n "$rest_b" ]; do 160 | head_a=${rest_a%%.*} 161 | head_b=${rest_b%%.*} 162 | rest_a=${rest_a#*.} 163 | rest_b=${rest_b#*.} 164 | 165 | if [ -z "$head_a" ] && [ -n "$head_b" ]; then 166 | return 0 167 | elif [ -n "$head_a" ] && [ -z "$head_b" ]; then 168 | return 1 169 | fi 170 | 171 | if [ "$head_a" = "$head_b" ]; then 172 | continue 173 | fi 174 | 175 | # If both are numbers then compare numerically 176 | if [ "$head_a" = "${head_a%[!0-9]*}" ] && [ "$head_b" = "${head_b%[!0-9]*}" ]; then 177 | [ "$head_a" -lt "$head_b" ] && return 0 || return 1 178 | # If only a is a number then return true (number has lower precedence than strings) 179 | elif [ "$head_a" = "${head_a%[!0-9]*}" ]; then 180 | return 0 181 | # If only b is a number then return false 182 | elif [ "$head_b" = "${head_b%[!0-9]*}" ]; then 183 | return 1 184 | # Finally if of identifiers is a number compare them lexically 185 | else 186 | test "$head_a" \< "$head_b" && return 0 || return 1 187 | fi 188 | done 189 | 190 | return 1 191 | } 192 | 193 | semver_gt() 194 | { 195 | if semver_lt "$1" "$2" || semver_eq "$1" "$2"; then 196 | return 1 197 | else 198 | return 0 199 | fi 200 | } 201 | 202 | semver_le() 203 | { 204 | semver_gt "$1" "$2" && return 1 || return 0 205 | } 206 | 207 | semver_ge() 208 | { 209 | semver_lt "$1" "$2" && return 1 || return 0 210 | } 211 | 212 | semver_sort() 213 | { 214 | if [ $# -le 1 ]; then 215 | echo "$1" 216 | return 217 | fi 218 | 219 | local pivot=$1 220 | local args_a=() 221 | local args_b=() 222 | 223 | shift 1 224 | 225 | for ver in "$@"; do 226 | if semver_le "$ver" "$pivot"; then 227 | args_a=( "${args_a[@]}" "$ver" ) 228 | else 229 | args_b=( "$ver" "${args_b[@]}" ) 230 | fi 231 | done 232 | 233 | args_a=( $(semver_sort "${args_a[@]}") ) 234 | args_b=( $(semver_sort "${args_b[@]}") ) 235 | echo "${args_a[@]}" "$pivot" "${args_b[@]}" 236 | } 237 | 238 | regex_match() 239 | { 240 | local string="$1 " 241 | local regexp="$2" 242 | local match 243 | match="$(eval "echo '$string' | grep -E -o '^[ \t]*($regexp)[ \t]+'")"; 244 | 245 | for i in $(seq 0 9); do 246 | unset "MATCHED_VER_$i" 247 | unset "MATCHED_NUM_$i" 248 | done 249 | unset REST 250 | 251 | if [ -z "$match" ]; then 252 | return 1 253 | fi 254 | 255 | local match_len=${#match} 256 | REST="${string:$match_len}" 257 | 258 | local part 259 | local i=1 260 | for part in $string; do 261 | local ver num 262 | ver="$(eval "echo '$part' | grep -E -o '$RE_VER' | head -n 1 | sed 's/ \t//g'")"; 263 | num=$(get_number "$ver") 264 | 265 | if [ -n "$ver" ]; then 266 | eval "MATCHED_VER_$i='$ver'" 267 | eval "MATCHED_NUM_$i='$num'" 268 | i=$(( i + 1 )) 269 | fi 270 | done 271 | 272 | return 0 273 | } 274 | 275 | # Normalizes rules string 276 | # 277 | # * replaces chains of whitespaces with single spaces 278 | # * replaces whitespaces around hyphen operator with "_" 279 | # * removes wildcards from version numbers (1.2.* -> 1.2) 280 | # * replaces "x" with "*" 281 | # * removes whitespace between operators and version numbers 282 | # * removes leading "v" from version numbers 283 | # * removes leading and trailing spaces 284 | normalize_rules() 285 | { 286 | echo " $1" \ 287 | | sed 's/\\t/ /g' \ 288 | | sed 's/ / /g' \ 289 | | sed 's/ \{2,\}/ /g' \ 290 | | sed 's/ - /_-_/g' \ 291 | | sed 's/\([~^<>=]\) /\1/g' \ 292 | | sed 's/\([ _~^<>=]\)v/\1/g' \ 293 | | sed 's/\.[xX*]//g' \ 294 | | sed 's/[xX]/*/g' \ 295 | | sed 's/^ //g' \ 296 | | sed 's/ $//g' 297 | } 298 | 299 | # Reads rule from provided string 300 | resolve_rule() 301 | { 302 | local rule operator operands 303 | rule="$1" 304 | operator="$( echo "$rule" | sed "s/$BRE_VERSION/#/g" )" 305 | operands=( $( echo "$rule" | grep -o "$BRE_VERSION") ) 306 | 307 | case "$operator" in 308 | '*') echo "all" ;; 309 | '#') echo "eq ${operands[0]}" ;; 310 | '=#') echo "eq ${operands[0]}" ;; 311 | '<#') echo "lt ${operands[0]}" ;; 312 | '>#') echo "gt ${operands[0]}" ;; 313 | '<=#') echo "le ${operands[0]}" ;; 314 | '>=#') echo "ge ${operands[0]}" ;; 315 | '#_-_#') echo "ge ${operands[0]}" 316 | echo "le ${operands[1]}" ;; 317 | '~#') echo "tilde ${operands[0]}" ;; 318 | '^#') echo "caret ${operands[0]}" ;; 319 | *) return 1 320 | esac 321 | } 322 | 323 | resolve_rules() 324 | { 325 | local rules 326 | rules="$(normalize_rules "$1")" 327 | IFS=' ' read -ra rules <<< "${rules:-all}" 328 | 329 | for rule in "${rules[@]}"; do 330 | resolve_rule "$rule" 331 | done 332 | } 333 | 334 | rule_eq() 335 | { 336 | local rule_ver="$1" 337 | local tested_ver="$2" 338 | 339 | semver_eq "$tested_ver" "$rule_ver" && return 0 || return 1; 340 | } 341 | 342 | rule_le() 343 | { 344 | local rule_ver="$1" 345 | local tested_ver="$2" 346 | 347 | semver_le "$tested_ver" "$rule_ver" && return 0 || return 1; 348 | } 349 | 350 | rule_lt() 351 | { 352 | local rule_ver="$1" 353 | local tested_ver="$2" 354 | 355 | semver_lt "$tested_ver" "$rule_ver" && return 0 || return 1; 356 | } 357 | 358 | rule_ge() 359 | { 360 | local rule_ver="$1" 361 | local tested_ver="$2" 362 | 363 | semver_ge "$tested_ver" "$rule_ver" && return 0 || return 1; 364 | } 365 | 366 | rule_gt() 367 | { 368 | local rule_ver="$1" 369 | local tested_ver="$2" 370 | 371 | semver_gt "$tested_ver" "$rule_ver" && return 0 || return 1; 372 | } 373 | 374 | rule_tilde() 375 | { 376 | local rule_ver="$1" 377 | local tested_ver="$2" 378 | 379 | if rule_ge "$rule_ver" "$tested_ver"; then 380 | local rule_major rule_minor 381 | rule_major=$(get_major "$rule_ver") 382 | rule_minor=$(get_minor "$rule_ver") 383 | 384 | if [ -n "$rule_minor" ] && rule_eq "$rule_major.$rule_minor" "$(get_number "$tested_ver")"; then 385 | return 0 386 | fi 387 | if [ -z "$rule_minor" ] && rule_eq "$rule_major" "$(get_number "$tested_ver")"; then 388 | return 0 389 | fi 390 | fi 391 | 392 | return 1 393 | } 394 | 395 | rule_caret() 396 | { 397 | local rule_ver="$1" 398 | local tested_ver="$2" 399 | 400 | if rule_ge "$rule_ver" "$tested_ver"; then 401 | local rule_major 402 | rule_major="$(get_major "$rule_ver")" 403 | 404 | if [ "$rule_major" != "0" ] && rule_eq "$rule_major" "$(get_number "$tested_ver")"; then 405 | return 0 406 | fi 407 | if [ "$rule_major" = "0" ] && rule_eq "$rule_ver" "$(get_number "$tested_ver")"; then 408 | return 0 409 | fi 410 | fi 411 | 412 | return 1 413 | } 414 | 415 | rule_all() 416 | { 417 | return 0 418 | } 419 | 420 | apply_rules() 421 | { 422 | local rules_string="$1" 423 | shift 424 | local versions=( "$@" ) 425 | 426 | # Loop over sets of rules (sets of rules are separated with ||) 427 | for ver in "${versions[@]}"; do 428 | rules_tail="$rules_string"; 429 | 430 | while [ -n "$rules_tail" ]; do 431 | head="${rules_tail%%||*}" 432 | 433 | if [ "$head" = "$rules_tail" ]; then 434 | rules_string="" 435 | else 436 | rules_tail="${rules_tail#*||}" 437 | fi 438 | 439 | #if [ -z "$head" ] || [ -n "$(echo "$head" | grep -E -x '[ \t]*')" ]; then 440 | #group=$(( $group + 1 )) 441 | #continue 442 | #fi 443 | 444 | rules="$(resolve_rules "$head")" 445 | 446 | # If specified rule cannot be recognised - end with error 447 | if [ $? -eq 1 ]; then 448 | exit 1 449 | fi 450 | 451 | if ! echo "$ver" | grep -q -E -x "[v=]?[ \t]*$RE_VER"; then 452 | continue 453 | fi 454 | 455 | ver=$(echo "$ver" | grep -E -x "$RE_VER") 456 | 457 | success=true 458 | allow_prerel=false 459 | if $FORCE_ALLOW_PREREL; then 460 | allow_prerel=true 461 | fi 462 | 463 | while read -r rule; do 464 | comparator="${rule%% *}" 465 | operand="${rule#* }" 466 | 467 | if [ -n "$(get_prerelease "$operand")" ] && semver_eq "$(get_number "$operand")" "$(get_number "$ver")" || [ "$rule" = "all" ]; then 468 | allow_prerel=true 469 | fi 470 | 471 | "rule_$comparator" "$operand" "$ver" 472 | if [ $? -eq 1 ]; then 473 | success=false 474 | break 475 | fi 476 | done <<< "$rules" 477 | 478 | if $success; then 479 | if [ -z "$(get_prerelease "$ver")" ] || $allow_prerel; then 480 | echo "$ver" 481 | break; 482 | fi 483 | fi 484 | done 485 | 486 | group=$(( group + 1 )) 487 | done 488 | } 489 | 490 | 491 | 492 | FORCE_ALLOW_PREREL=false 493 | USAGE="Usage: $0 [-r ] [... ] 494 | 495 | Omitting s reads them from STDIN. 496 | Omitting -r simply sorts the versions according to semver ordering." 497 | 498 | while getopts ar:h o; do 499 | case "$o" in 500 | a) FORCE_ALLOW_PREREL=true ;; 501 | r) RULES_STRING="$OPTARG||";; 502 | h) echo "$USAGE" && exit ;; 503 | ?) echo "$USAGE" && exit 1;; 504 | esac 505 | done 506 | 507 | shift $(( OPTIND-1 )) 508 | 509 | VERSIONS=( ${@:-$(cat -)} ) 510 | 511 | # Sort versions 512 | VERSIONS=( $(semver_sort "${VERSIONS[@]}") ) 513 | 514 | if [ -z "$RULES_STRING" ]; then 515 | printf '%s\n' "${VERSIONS[@]}" 516 | else 517 | apply_rules "$RULES_STRING" "${VERSIONS[@]}" 518 | fi 519 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodenv/nodenv-package-json-engine", 3 | "version": "3.0.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@nodenv/devutil": { 8 | "version": "0.1.1", 9 | "resolved": "https://registry.npmjs.org/@nodenv/devutil/-/devutil-0.1.1.tgz", 10 | "integrity": "sha512-vvwlZ+fjjhCEOnjA4M/gkm8CwzRkKLOiBH5kaEPJLLDOZs4UIPfQ24+Un3reQzF0eOQLDL0SYAieLDITUKxwuQ==", 11 | "dev": true 12 | }, 13 | "@nodenv/nodenv": { 14 | "version": "1.5.0", 15 | "resolved": "https://registry.npmjs.org/@nodenv/nodenv/-/nodenv-1.5.0.tgz", 16 | "integrity": "sha512-/gxxII3r0SR+FjpWVqxmWsYTTRUldQPcvU5AHgKt/v/xjpg+q88Lk1zyuGTA5PrEOWTW/d01JDfHyHMnPtSjdQ==", 17 | "dev": true 18 | }, 19 | "JSON.sh": { 20 | "version": "0.3.3", 21 | "resolved": "https://registry.npmjs.org/JSON.sh/-/JSON.sh-0.3.3.tgz", 22 | "integrity": "sha1-OgVKdi8yq5iamSZJZ1QV4JU+4Hg=" 23 | }, 24 | "bats": { 25 | "version": "1.12.0", 26 | "resolved": "https://registry.npmjs.org/bats/-/bats-1.12.0.tgz", 27 | "integrity": "sha512-1HTv2n+fjn3bmY9SNDgmzS6bjoKtVlSK2pIHON5aSA2xaqGkZFoCCWP46/G6jm9zZ7MCi84mD+3Byw4t3KGwBg==", 28 | "dev": true 29 | }, 30 | "bats-assert": { 31 | "version": "github:jasonkarns/bats-assert-1#8200039faf9790c05d9865490c97a0e101b9c80f", 32 | "from": "github:jasonkarns/bats-assert-1", 33 | "dev": true 34 | }, 35 | "bats-support": { 36 | "version": "github:jasonkarns/bats-support#004e707638eedd62e0481e8cdc9223ad471f12ee", 37 | "from": "github:jasonkarns/bats-support", 38 | "dev": true 39 | }, 40 | "sh-semver": { 41 | "version": "github:qzb/sh-semver#a962f9d2f3d26b2da3d4116661cc207fe50bb99c", 42 | "from": "github:qzb/sh-semver" 43 | }, 44 | "shellcheck": { 45 | "version": "0.4.4", 46 | "resolved": "https://registry.npmjs.org/shellcheck/-/shellcheck-0.4.4.tgz", 47 | "integrity": "sha512-d7LfrgEcGZes825x5Yehm6MkpfSQaDg4nP0VZEv7E2qX84zQXDbCdDvYJg9V9OByG2+h98Ol7nndixgGOj8cRQ==", 48 | "dev": true 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodenv/nodenv-package-json-engine", 3 | "version": "3.0.3", 4 | "description": "Activate a nodenv node version from package.json engines", 5 | "homepage": "https://github.com/nodenv/nodenv-package-json-engine#readme", 6 | "license": "MIT", 7 | "author": "Adam Hull (http://hurrymaplelad.com)", 8 | "contributors": [ 9 | "Jason Karns (http://jason.karns.name)" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/nodenv/nodenv-package-json-engine.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/nodenv/nodenv-package-json-engine/issues" 17 | }, 18 | "directories": { 19 | "bin": "bin", 20 | "test": "test" 21 | }, 22 | "files": [ 23 | "bin", 24 | "etc", 25 | "libexec" 26 | ], 27 | "scripts": { 28 | "lint": "git ls-files bin etc libexec test/*.bash | grep -Ev '(semver|JSON).sh' | xargs shellcheck", 29 | "test": "bats ${CI:+--tap} test", 30 | "posttest": "npm run lint", 31 | "postversion": "git push --follow-tags", 32 | "relnotes": "changelog -- bin etc libexec" 33 | }, 34 | "devDependencies": { 35 | "@nodenv/devutil": "^0.1.1", 36 | "@nodenv/nodenv": "^1.5.0", 37 | "bats": "^1.12.0", 38 | "bats-assert": "jasonkarns/bats-assert-1", 39 | "bats-support": "jasonkarns/bats-support", 40 | "shellcheck": "^0.4.4" 41 | }, 42 | "dependencies": { 43 | "sh-semver": "qzb/sh-semver", 44 | "JSON.sh": "~0.3.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/fixtures/node-x.y.z/bin/node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodenv/nodenv-package-json-engine/1613ac285eb080305344419dd9c2592e3432a96a/test/fixtures/node-x.y.z/bin/node -------------------------------------------------------------------------------- /test/fixtures/nodenv_root/plugins/nodenv-package-json-engine: -------------------------------------------------------------------------------- 1 | ../../../.. -------------------------------------------------------------------------------- /test/fixtures/nodenv_root/versions/4.0.0: -------------------------------------------------------------------------------- 1 | ../../node-x.y.z -------------------------------------------------------------------------------- /test/fixtures/nodenv_root/versions/4.2.1: -------------------------------------------------------------------------------- 1 | ../../node-x.y.z -------------------------------------------------------------------------------- /test/fixtures/nodenv_root/versions/5.0.0: -------------------------------------------------------------------------------- 1 | ../../node-x.y.z -------------------------------------------------------------------------------- /test/nodenv-package-json-engine.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test 'Recognizes simple node version specified in package.json engines' { 6 | in_package_for_engine 4.2.1 7 | 8 | run nodenv version 9 | assert_success 10 | assert_output "4.2.1 => node-x.y.z (set by $PWD/package.json#engines.node)" 11 | } 12 | 13 | @test 'Prefers the greatest installed version matching a range' { 14 | in_package_for_engine '^4.0.0' 15 | 16 | run nodenv version 17 | assert_success 18 | assert_output "4.2.1 => node-x.y.z (set by $PWD/package.json#engines.node)" 19 | } 20 | 21 | @test 'Ignores non-matching installed versions' { 22 | in_package_for_engine '^1.0.0' 23 | 24 | run nodenv version 25 | assert_failure 26 | assert_output - <<-MSG 27 | package-json-engine: version satisfying \`^1.0.0' is not installed 28 | MSG 29 | } 30 | 31 | @test 'Prefers nodenv-local over package.json' { 32 | in_package_for_engine 4.2.1 33 | nodenv local 5.0.0 34 | 35 | run nodenv version 36 | assert_success 37 | assert_output "5.0.0 => node-x.y.z (set by $PWD/.node-version)" 38 | } 39 | 40 | @test 'Prefers nodenv-shell over package.json' { 41 | in_package_for_engine 4.2.1 42 | 43 | NODENV_VERSION=5.0.0 run nodenv version 44 | assert_success 45 | assert_output "5.0.0 => node-x.y.z (set by NODENV_VERSION environment variable)" 46 | } 47 | 48 | @test 'Prefers package.json over nodenv-global' { 49 | in_package_for_engine 4.2.1 50 | nodenv global 5.0.0 51 | 52 | run nodenv version-name 53 | assert_success 54 | assert_output '4.2.1' 55 | } 56 | 57 | @test 'Is not confused by nodenv-shell shadowing nodenv-global' { 58 | in_package_for_engine 4.2.1 59 | nodenv global 5.0.0 60 | 61 | NODENV_VERSION=5.0.0 run nodenv version 62 | assert_success 63 | assert_output "5.0.0 => node-x.y.z (set by NODENV_VERSION environment variable)" 64 | } 65 | 66 | @test 'Does not match arbitrary "node" key in package.json' { 67 | in_package_with_babel_env 68 | 69 | run nodenv version-name 70 | 71 | assert_success 72 | assert_output 'system' 73 | } 74 | 75 | @test 'Handles missing package.json' { 76 | in_example_package 77 | 78 | run nodenv version-name 79 | 80 | assert_success 81 | assert_output 'system' 82 | } 83 | 84 | @test 'Does not fail with unreadable package.json' { 85 | in_example_package 86 | touch package.json 87 | chmod -r package.json 88 | 89 | run nodenv version-name 90 | 91 | assert_success 92 | assert_output 'system' 93 | } 94 | 95 | @test 'Does not fail with non-file package.json' { 96 | in_example_package 97 | mkdir package.json 98 | 99 | run nodenv version-name 100 | 101 | assert_success 102 | assert_output 'system' 103 | } 104 | 105 | @test 'Does not fail with empty or malformed package.json' { 106 | in_example_package 107 | 108 | # empty 109 | touch package.json 110 | run nodenv version-name 111 | assert_success 112 | assert_output 'system' 113 | 114 | # non json 115 | echo "foo" > package.json 116 | run nodenv version-name 117 | assert_success 118 | assert_output 'system' 119 | 120 | # malformed 121 | echo "{" > package.json 122 | run nodenv version-name 123 | assert_success 124 | assert_output 'system' 125 | } 126 | 127 | @test 'Handles multiple occurrences of "node" key' { 128 | in_example_package 129 | cat << JSON > package.json 130 | { 131 | "engines": { 132 | "node": "4.2.1" 133 | }, 134 | "presets": [ 135 | ["env", { 136 | "targets": { 137 | "node": "current" 138 | } 139 | }] 140 | ] 141 | } 142 | JSON 143 | 144 | run nodenv version-name 145 | assert_success 146 | assert_output '4.2.1' 147 | } 148 | -------------------------------------------------------------------------------- /test/test_helper.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | load '../node_modules/bats-support/load' 4 | load '../node_modules/bats-assert/load' 5 | 6 | setup() { 7 | # common nodenv setup 8 | unset NODENV_VERSION 9 | 10 | local node_modules_bin=$BATS_TEST_DIRNAME/../node_modules/.bin 11 | 12 | export PATH="$node_modules_bin:/usr/bin:/bin:/usr/sbin:/sbin" 13 | 14 | export NODENV_ROOT="$BATS_TEST_DIRNAME/fixtures/nodenv_root" 15 | 16 | # custom setup 17 | EXAMPLE_PACKAGE_DIR="$BATS_TMPDIR/example_package" 18 | mkdir -p "$EXAMPLE_PACKAGE_DIR" 19 | cd "$EXAMPLE_PACKAGE_DIR" || return 1 20 | } 21 | 22 | teardown() { 23 | rm -f "$EXAMPLE_PACKAGE_DIR"/.node-version 24 | rm -rf "$EXAMPLE_PACKAGE_DIR"/package.json 25 | rm -f "$NODENV_ROOT/version" 26 | } 27 | 28 | in_example_package() { 29 | cd "$EXAMPLE_PACKAGE_DIR" || return 1 30 | } 31 | 32 | in_package_for_engine() { 33 | in_example_package 34 | cat << JSON > package.json 35 | { 36 | "engines": { 37 | "node": "$1" 38 | } 39 | } 40 | JSON 41 | } 42 | 43 | in_package_with_babel_env() { 44 | in_example_package 45 | cat << JSON > package.json 46 | { 47 | "presets": [ 48 | ["env", { 49 | "targets": { 50 | "node": "current" 51 | } 52 | }] 53 | ] 54 | } 55 | JSON 56 | } 57 | --------------------------------------------------------------------------------