Dynamically interpret semver expression in local dirs package.json

This commit is contained in:
edwmurph 2018-04-09 17:25:30 -04:00 committed by edwmurph
parent b81c120e0c
commit 97bd5911ef
13 changed files with 857 additions and 5 deletions

378
nvm.sh
View File

@ -299,6 +299,357 @@ nvm_find_up() {
nvm_echo "${path_}"
}
nvm_string_contains_regexp() {
local string
string="${1-}"
local regexp
regexp="${2-}"
if [ -z "${string-}" ] || [ -z "${regexp-}" ]; then
return 1
fi
# e.g. "nvm_string_contains_regexp abbc ^aa?b+.$" returns 0
command printf "%s" "$string" | command awk "/$regexp/{ exit 0 }{ exit 1 }"
}
# scoped function returns nothing or a normalized semver adhering to the following grammar:
#
# semver ::= comparator_set ( ' || ' comparator_set )*
# comparator_set ::= comparator ( ' ' comparator )*
# comparator ::= ( '<' | '<=' | '>' | '>=' | '' ) [0-9]+ '.' [0-9]+ '.' [0-9]+
nvm_validate_semver() { (
semver=$(command printf "%s" "${1-}" | command tr '||' '\n')
[ -n "$semver" ] || return 1
validated_semver='';
while [ -n "$semver" ]; do
comparator_set=$(command printf "%s" "$semver" | head -n1)
semver=$(command printf "%s" "$semver" | tail -n +2)
[ -n "$comparator_set" ] || continue
# convert comparators into required grammar
validated_comparator_set=$(command printf " %s " "$comparator_set" |
# exactly 1 space is needed before and after every comparator (including the first and last comparators)
command sed -E 's/\011/ /g;s/ +/ /g' |
# normalize all wildcards to `x`
command sed -E 's/X|\*/x/g' |
# ` 1 ` => ` 1.x.x `
# ` x ` => ` x.x.x `
command sed -E 's/ ([0-9]+|x) / \1.x.x /g' |
# ` 1.2 ` => ` 1.2.x `
# ` 1.x ` => ` 1.x.x `
# ` x.x ` => ` x.x.x `
command sed -E 's/ (([0-9]+|x)\.([0-9]+|x)) / \1.x /g' |
# ` 1.2.3 - 1.2.4 ` => ` >=1.2.3 <=1.2.4 `
command sed -E 's/ (([0-9]+|x)\.([0-9]+|x)\.([0-9]+|x)) ?\- ?(([0-9]+|x)\.([0-9]+|x)\.([0-9]+|x)) / >=\1 <=\5 /g' |
# ` > 1.2.3 ` => ` >1.2.3 `
# ` < 1.2.5 ` => ` <1.2.5 `
# ` <= 1.2.3 ` => ` <=1.2.3 `
# ` >= 1.2.3 ` => ` >=1.2.3 `
# ` = 1.2.3 ` => ` =1.2.3 `
# ` ~ 1.2.3 ` => ` ~1.2.3 `
# ` ^ 1.2.3 ` => ` ^1.2.3 `
command sed -E 's/ (<|>|<=|>=|=|~|\^) (([0-9]+|x)\.([0-9]+|x)\.([0-9]+|x)) / \1\2 /g' |
# ` =1.2.3 ` => ` 1.2.3 `
command sed -E 's/ =//g' |
# trim leading/trailing spaces
command sed -E 's/^ //;s/ $//' |
# handle conversions of comparators with '^' or '~' or 'x' into required grammar
# ` ^0.0.1 ` => ` >=0.0.1 <0.0.2 `
# ` ^0.1.2 ` => ` >=0.1.2 <0.2.0 `
# ` ^1.2.3 ` => ` >=1.2.3 <2.0.0 `
# ` ~0.0.1 ` => ` >=0.0.1 <0.1.0 `
# ` ~0.1.2 ` => ` >=0.1.2 <0.2.0 `
# ` ~1.2.3 ` => ` >=1.2.3 <1.3.0 `
# ` x.x.x ` => ` >0.0.0 `
# ` x.x.1 ` => ` >0.0.0 `
# ` x.1.x ` => ` >0.0.0 `
# ` x.1.2 ` => ` >0.0.0 `
# ` 1.2.x ` => ` >=1.2.0 <1.3.0 `
# ` 1.x.1 ` => ` >=1.0.0 <2.0.0 ` NOTE the last "1" is ignored in this grammar
# ` 1.x.x ` => ` >=1.0.0 <2.0.0 `
command awk '{
if ( ! match($0, /[\^~x]/) ) {
print $0
exit 0
}
split($0, comparators, / /)
output=""
for (i = 1; i <= length(comparators); i++) {
comparator=comparators[i]
if ( match(comparator, /^([\^~>]|>=)?x.[0-9x]+.[0-9x]$/ ) ) {
comparator=">0.0.0"
} else if ( match(comparator, /^([\^~<>]|>=|<=)?[0-9]+.x.[0-9x]$/) ) {
split(comparator, a, /\./);
if ( match(comparator, /^([\^~<>]|>=|<=).*$/ ) ) {
comparator=a[1] ".0.0";
} else {
comparator=">=" a[1] ".0.0 <" a[1]+1 ".0.0"
}
} else if ( match(comparator, /^([\^~<>]|<=|>=)?[0-9]+.[0-9]+.x$/) ) {
split(comparator, a, /\./);
if ( match(comparator, /^([\^~<>]|<=|>=).*$/ ) ) {
comparator=a[1] "." a[2] ".0";
} else {
comparator=">=" a[1] "." a[2] ".0 <" a[1] "." a[2]+1 ".0"
}
}
if ( match(comparator, /^\^/) ) {
if ( match(comparator, /^\^0.0.[0-9]+$/) ) {
version=substr(comparator,2);
split(version, a, /\./);
output=output ">=" version " <0.0." a[3]+1 " ";
} else if ( match(comparator, /^\^0.[0-9]+.[0-9]+$/) ) {
version=substr(comparator,2);
split(version, a, /\./);
output=output ">=" version " <0." a[2]+1 ".0 ";
} else if ( match(comparator, /^\^[0-9]+.[0-9]+.[0-9]+$/) ) {
version=substr(comparator,2);
split(version, a, /\./);
output=output ">=" version " <" a[1]+1 ".0.0 ";
}
} else if ( match(comparator, /^~/) ) {
if ( match(comparator, /^~0.0.[0-9]+$/) ) {
version=substr(comparator,2)
split(version, a, /\./)
output=output ">=" version " <0." a[2]+1 ".0 "
} else if ( match(comparator, /^~0.[0-9]+.[0-9]+$/) ) {
version=substr(comparator,2)
split(version, a, /\./)
output=output ">=" version " <0." a[2]+1 ".0 "
} else if ( match(comparator, /^~[0-9]+.[0-9]+.[0-9]+$/) ) {
version=substr(comparator,2)
split(version, a, /\./)
output=output ">=" version " <" a[1] "." a[2]+1 ".0 "
}
} else {
output=output comparator " "
}
}
print output
}' |
# remove leading/trailing spaces
command sed -E 's/^ +//;s/ +$//'
)
# only comparator_sets composed of the required grammar are marked as valid
if nvm_string_contains_regexp "$validated_comparator_set" '^( ?(<|<=|>|>=)?[0-9]+\.[0-9]+\.[0-9]+)+$'; then
validated_semver="$validated_semver || $validated_comparator_set"
else
return 1
fi
done
nvm_echo "$(command printf "%s" "$validated_semver" | command sed -E 's/^ \|\| //')"
) }
nvm_interpret_complex_semver() { (
[ -n "${1-}" ] || return 1
# Validate incoming semver and transform it into the grammar that is expected by the following logic
valid_transformed_semver=$(nvm_validate_semver "${1-}")
[ -n "$valid_transformed_semver" ] || ( nvm_err "invalid semantic version: '${1-}'" && return 1 )
# Iterate through the comparator_sets in the semver.
# For each comparator_set, evaluate it to the highest compatible node version.
# Output this function with the highest node version among all the node versions collected in the previous step.
valid_transformed_semver=$(command printf "%s" "$valid_transformed_semver" | command tr '||' '\n')
highest_compatible_node_versions=''
# TODO add tests verifying this logic correctly gets list of node versions and that the order is from oldest to newest
# list of node versions is sorted from oldest to newest
remote_node_versions=$(nvm_ls_remote | grep -o 'v[0-9]\+\.[0-9]\+\.[0-9]\+')
[ -n "$remote_node_versions" ] || ( nvm_err "failure retrieving remote node versions" && return 1 )
while [ -n "$valid_transformed_semver" ]; do
remote_node_versions_copy=$(command printf "%s" "$remote_node_versions")
comparator_set=$(command printf "%s" "$valid_transformed_semver" | head -n1 | command sed -E 's/^ +//;s/ +$//')
valid_transformed_semver=$(command printf "%s" "$valid_transformed_semver" | tail -n +2)
[ -n "$comparator_set" ] || continue
# variable indicating the state of the current comparator_set
TRUE=0;FALSE=1
node_version_compatible=$FALSE
no_remote_version_will_satisfy_all_comparators=$FALSE
# compare each remote node version (from newest to oldest) to each comparator and stop when you find the newest compatible remote node version
# $node_version_compatible being true indicates that we have found a remote node version that satisfies all the comparators.
# $no_remote_version_will_satisfy_all_comparators being true indicates that there is no remote node version that will satisfy all comparators.
while [ -n "$remote_node_versions_copy" ] && [ $node_version_compatible -eq $FALSE ] && [ $no_remote_version_will_satisfy_all_comparators -eq $FALSE ]; do
# current node version being iterated on
current_remote_node_version=$(command printf "%s" "$remote_node_versions_copy" | tail -n1 | command sed -E 's/^ +//;s/ +$//' | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+$')
# remove current_remote_node_version from remote_node_versions_copy so that the next iteration will check the next newest node version
remote_node_versions_copy=$(command printf "%s" "$remote_node_versions_copy" | sed '$d')
[ -n "$current_remote_node_version" ] || continue
node_version_compatible=$FALSE # initialize state
# check if current_remote_node_version is compatible with every comparator in comparator_set.
# stop checking upon finding a comparator that is not satisfied by the current_remote_node_version.
comparator_set_copy=$(command printf "%s" "$comparator_set" | command tr ' ' '\n')
while [ -n "$comparator_set_copy" ]; do
comparator=$(command printf "%s" "$comparator_set_copy" | head -n1 | command sed -E 's/^ +//;s/ +$//')
comparator_set_copy=$(command printf "%s" "$comparator_set_copy" | tail -n +2)
[ -n "$comparator" ] || continue
# if comparator is satisfied by current_remote_node_version, update state variable node_version_compatible
stripped_version_from_comparator="$(command printf "%s" "$comparator" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+$')"
if nvm_string_contains_regexp "$comparator" '^[0-9]+\.[0-9]+\.[0-9]+$'; then
if [ "$comparator" = "$current_remote_node_version" ]; then
# comparator is looking for an exact match and the current_remote_node_version is that semver so this comparator is satisfied.
node_version_compatible=$TRUE
elif nvm_version_greater "$comparator" "$current_remote_node_version"; then
# looking for a version that is equal to $comparator but $current_remote_node_version is less than $comparator so there is no point continuing.
node_version_compatible=$FALSE
no_remote_version_will_satisfy_all_comparators=$TRUE
else
node_version_compatible=$FALSE
fi
elif nvm_string_contains_regexp "$comparator" '^<=[0-9]+\.[0-9]+\.[0-9]+$'; then
if nvm_version_greater_than_or_equal_to "$stripped_version_from_comparator" "$current_remote_node_version"; then
# current_remote_node_version is less and or equal to the current comparator version number so this comparator is satisfied.
node_version_compatible=$TRUE
else
node_version_compatible=$FALSE
fi
elif nvm_string_contains_regexp "$comparator" '^>=[0-9]+\.[0-9]+\.[0-9]+$'; then
if nvm_version_greater_than_or_equal_to "$current_remote_node_version" "$stripped_version_from_comparator"; then
node_version_compatible=$TRUE
else
node_version_compatible=$FALSE
no_remote_version_will_satisfy_all_comparators=$TRUE
fi
elif nvm_string_contains_regexp "$comparator" '^<[0-9]+\.[0-9]+\.[0-9]+$'; then
if nvm_version_greater "$stripped_version_from_comparator" "$current_remote_node_version"; then
node_version_compatible=$TRUE
else
node_version_compatible=$FALSE
fi
elif nvm_string_contains_regexp "$comparator" '^>[0-9]+\.[0-9]+\.[0-9]+$'; then
if nvm_version_greater "$current_remote_node_version" "$stripped_version_from_comparator"; then
node_version_compatible=$TRUE
else
node_version_compatible=$FALSE
no_remote_version_will_satisfy_all_comparators=$TRUE
fi
else
node_version_compatible=$FALSE
fi
# stop checking if all comparators are compatible with current_remote_node_version upon finding one that is incompatible
[ $node_version_compatible -eq $FALSE ] && comparator_set_copy=''
done # while [ -n "$comparator_set_copy" ]; do
# stop iterating through remote_node_versions_copy for this comparator_set upon finding the first remote node version that is compatible with all comparators.
[ $node_version_compatible -eq $TRUE ] && highest_compatible_node_versions="$highest_compatible_node_versions $current_remote_node_version"
done # while [ -n "$remote_node_versions_copy" ] && [ $node_version_compatible -eq $FALSE ] && [ $no_remote_version_will_satisfy_all_comparators -eq $FALSE ]; do
done
highest_compatible_node_version='0.0.0'
highest_compatible_node_versions=$(command printf "%s" "$highest_compatible_node_versions" | command tr ' ' '\n')
while [ -n "$highest_compatible_node_versions" ]; do
compatible_node_version=$(command printf "%s" "$highest_compatible_node_versions" | head -n1 | command sed -E 's/^ +//;s/ +$//')
highest_compatible_node_versions=$(command printf "%s" "$highest_compatible_node_versions" | tail -n +2)
[ -n "$compatible_node_version" ] || continue
if nvm_version_greater "$compatible_node_version" "$highest_compatible_node_version"; then
highest_compatible_node_version="$compatible_node_version"
fi
done
[ "$highest_compatible_node_version" != '0.0.0' ] && nvm_echo "$highest_compatible_node_version"
) }
nvm_find_package_json() { (
dir="$(nvm_find_up 'package.json')"
if [ -e "${dir}/package.json" ]; then
nvm_echo "${dir}/package.json"
fi
) }
# extracts engines.node value from package.json exactly except:
# - removes all line breaks and carriage returns
# - normalizes all consecutive whitespace to 1 occurrence
# - must match regexp: "[|<> [:alnum:].^=~*-]\+"
nvm_get_node_from_pkg_json() { (
package_json_contents=${1-}
engines_node_value=''
open_brackets=0
closed_brackets=0
in_quotes='false'
command printf "%s" "$package_json_contents" \
| command tr -d '\n\r' \
| command tr '\t' ' ' \
| command tr -s ' ' \
| nvm_grep -o '"engines": \?{ \?".*' \
| nvm_grep -o '{.*' \
| nvm_grep -o . \
| while read -r i; do
engines_node_value="$engines_node_value$i"
if [ "$i" = '"' ]; then
if [ "$in_quotes" = 'false' ]; then
in_quotes='true'
else
in_quotes='false'
fi
# spaces are interpretted as '' here but they need to be retained
elif [ "$i" = '' ]; then
engines_node_value="$engines_node_value "
elif [ "$in_quotes" = 'false' ]; then
if [ "$i" = '{' ]; then
open_brackets=$((open_brackets+1))
elif [ "$i" = '}' ]; then
closed_brackets=$((closed_brackets+1))
fi
fi
if [ "$open_brackets" -ne 0 ] && [ "$open_brackets" -eq "$closed_brackets" ]; then
command printf "%s" "$engines_node_value" \
| nvm_grep -o '"node": \?"[|<> [:alnum:].^=~*-]\+"' \
| command tr -d '"' \
| command awk -F: '{ print $2 }' \
| command sed 's/^ //; s/ $//; s/^ //'
return 0
fi
done
return 2
) }
nvm_package_json_version() {
export PKG_JSON_VERSION=''
local pkg_json_path
pkg_json_path="$(nvm_find_package_json)"
if [ ! -e "${pkg_json_path}" ]; then
nvm_err "No package.json file found"
return 1
fi
PKG_JSON_VERSION=$(nvm_get_node_from_pkg_json "$(command cat "$pkg_json_path")" || command printf '')
if [ ! -n "${PKG_JSON_VERSION}" ]; then
nvm_echo "Warning: could not retrieve engines.node semver expression in package.json file found at \"${pkg_json_path}\""
return 2
else
nvm_echo "Found '${pkg_json_path}' with semver expression <${PKG_JSON_VERSION}>"
# attempt complex semver range evaluation
PKG_JSON_VERSION=$(nvm_interpret_complex_semver "$PKG_JSON_VERSION")
if [ ! -n "${PKG_JSON_VERSION}" ]; then
nvm_echo "Warning: could not interpret engines.node semver expression obtained from package.json file."
return 2
fi
fi
}
nvm_find_nvmrc() {
local dir
@ -430,6 +781,7 @@ nvm_version() {
local PATTERN
PATTERN="${1-}"
local VERSION
# The default version is the current one
if [ -z "${PATTERN}" ]; then
PATTERN='current'
@ -2596,14 +2948,19 @@ nvm() {
shift
fi
else
nvm_rc_version
if [ $version_not_provided -eq 1 ] && [ -z "$NVM_RC_VERSION" ]; then
nvm_rc_version || nvm_package_json_version
if [ -n "${NVM_RC_VERSION-}" ]; then
provided_version="$NVM_RC_VERSION"
elif [ -n "${PKG_JSON_VERSION-}" ]; then
provided_version="$PKG_JSON_VERSION"
elif [ $version_not_provided -eq 1 ]; then
unset NVM_RC_VERSION
unset PKG_JSON_VERSION
>&2 nvm --help
return 127
fi
provided_version="$NVM_RC_VERSION"
unset NVM_RC_VERSION
unset PKG_JSON_VERSION
fi
elif [ $# -gt 0 ]; then
shift
@ -2947,12 +3304,20 @@ nvm() {
if [ -n "${NVM_LTS-}" ]; then
VERSION="$(nvm_match_version "lts/${NVM_LTS:-*}")"
elif [ -z "${PROVIDED_VERSION-}" ]; then
nvm_rc_version
nvm_rc_version || nvm_package_json_version
if [ -n "${NVM_RC_VERSION-}" ]; then
PROVIDED_VERSION="$NVM_RC_VERSION"
VERSION="$(nvm_version "$PROVIDED_VERSION")"
elif [ -n "${PKG_JSON_VERSION-}" ]; then
PROVIDED_VERSION="$PKG_JSON_VERSION"
else
unset NVM_RC_VERSION
unset PKG_JSON_VERSION
>&2 nvm --help
return 127
fi
VERSION="$(nvm_version "$PROVIDED_VERSION")"
unset NVM_RC_VERSION
unset PKG_JSON_VERSION
else
VERSION="$(nvm_match_version "$PROVIDED_VERSION")"
fi
@ -3543,6 +3908,9 @@ nvm() {
nvm_sanitize_path nvm_has_colors nvm_process_parameters \
node_version_has_solaris_binary iojs_version_has_solaris_binary \
nvm_curl_libz_support nvm_command_info \
nvm_get_node_from_pkg_json nvm_find_package_json nvm_package_json_version \
nvm_interpret_complex_semver nvm_validate_semver \
nvm_string_contains_regexp \
> /dev/null 2>&1
unset NVM_RC_VERSION NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR NVM_DIR \
NVM_CD_FLAGS NVM_BIN NVM_MAKE_JOBS \

50
semver.md Normal file
View File

@ -0,0 +1,50 @@
# Node semantic version interpretation documentation
Node versions are dynamically interpretted from the semver expression that is extracted from the local package.json file. The algorithm for doing this is expressed below. Each step is isolated into its own function to make testing and debugging easier.
## 1. Extract the semver expression located in the engines.node value of the local package.json file.
#### Required input grammar for semver expression extracted from package.json:
> Grammar is copied from https://docs.npmjs.com/misc/semver
> ```
> range-set ::= range ( logical-or range ) *
> logical-or ::= ( ' ' ) * '||' ( ' ' ) *
> range ::= hyphen | simple ( ' ' simple ) * | ''
> hyphen ::= partial ' - ' partial
> simple ::= primitive | partial | tilde | caret
> primitive ::= ( '<' | '>' | '>=' | '<=' | '=' | ) partial
> partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
> xr ::= 'x' | 'X' | '*' | nr
> nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
> tilde ::= '~' partial
> caret ::= '^' partial
> qualifier ::= ( '-' pre )? ( '+' build )?
> pre ::= parts
> build ::= parts
> parts ::= part ( '.' part ) *
> part ::= nr | [-0-9A-Za-z]+
> ```
> Lazy grammar validation is used at this point. Basically any string in the engines.node value will be accepted at this point that matches the following regexp:
> "[|<> [:alnum:].^=~*-]\+"
>
> NOTE: all whitespace inside the engines.node value is normalized to be a single space in this step.
## 2. Check that the extracted semver expression from the previous step matches the above grammar. If so, normalize it into the following grammar that is expected by the interpretation logic.
#### Required input grammar for internal interpretation logic:
> ```
> semver ::= comparator_set ( ' || ' comparator_set )*
> comparator_set ::= comparator ( ' ' comparator )*
> comparator ::= ( '<' | '<=' | '>' | '>=' | '' ) [0-9]+ '.' [0-9]+ '.' [0-9]+
> ```
## 3. Interpret the normalized semver expression.
> 1. Resolve each comparator set to the newest compatible node version
> - iterate through node versions from newest to oldest
> - find the first node version that satisfies all comparators
> - if reached a point where no older node versions will satisfy comparator, stop iterating through node versions.
> 2. Choose the newest node version among the resolved node versions from the previous step.

View File

@ -0,0 +1,242 @@
#!/bin/sh
die () { printf "$@" ; exit 1; }
\. ../../../nvm.sh
BASIC_VERSIONS='1.2.3
10.21.32
x.x.x
X.X.X
*.*.*
1.2
10.21
x.x
X.X
*.*
1
10
x
*
X'
SEMVER_OPERATORS='v
=
<
>
<=
>=
~
^'
# Dynamically generate list of semver expressions to test inside each package.json template.
# Each semver added to this list is paired with the expected output when executed with nvm_get_node_from_pkg_json.
# Most valid semvers will expecte the output to be the same as the input but there are some valid semvers with certain
# sequences of tabs/spaces whose outputs are slightly different from their inputs (documented below).
# Format for test cases is input and expected output separated by a colon
# INPUT:EXPECTED_OUTPUT
TEST_SEMVERS=''
BASIC_VERSIONS_COPY=$(echo "$BASIC_VERSIONS")
while [ -n "$BASIC_VERSIONS_COPY" ]; do
BASIC_VERSION=$(echo "$BASIC_VERSIONS_COPY" | head -n1)
BASIC_VERSIONS_COPY=$(echo "$BASIC_VERSIONS_COPY" | tail -n +2)
# add test semver with just the version and no comparator
TEST_SEMVERS="$TEST_SEMVERS
$BASIC_VERSION:$BASIC_VERSION"
SEMVER_OPERATORS_COPY=$(echo "$SEMVER_OPERATORS")
while [ -n "$SEMVER_OPERATORS_COPY" ]; do
SEMVER_OPERATOR=$(echo "$SEMVER_OPERATORS_COPY" | head -n1)
SEMVER_OPERATORS_COPY=$(echo "$SEMVER_OPERATORS_COPY" | tail -n +2)
# add test semver with version adjacent to operator
# add test semver with version separated from operator by one space
TEST_SEMVERS="$TEST_SEMVERS
$SEMVER_OPERATOR$BASIC_VERSION:$SEMVER_OPERATOR$BASIC_VERSION
$SEMVER_OPERATOR $BASIC_VERSION:$SEMVER_OPERATOR $BASIC_VERSION"
done
done
# add valid basic test semvers with hyphen ranges
TEST_SEMVERS="$TEST_SEMVERS
1.2.3 - 1.2.4:1.2.3 - 1.2.4
10.21.32 - 10.21.33:10.21.32 - 10.21.33
1.2 - 1.3:1.2 - 1.3
10.21 - 10.22:10.21 - 10.22
1 - 2:1 - 2
10 - 11:10 - 11"
# add more complex test semvers with just one comparator set
TEST_SEMVERS="$TEST_SEMVERS
1.2.3 1.2.4:1.2.3 1.2.4
1.2 1.3:1.2 1.3
1 2:1 2
>1.2.3 <=1.3.0:>1.2.3 <=1.3.0"
# add test semvers with multiple comparator sets
TEST_SEMVERS="$TEST_SEMVERS
1.2.3 || 1.2.4:1.2.3 || 1.2.4
1.2 || 1.3:1.2 || 1.3
1 || 2:1 || 2"
# add test semvers that will be successfully extracted from package.json
# but will be marked invalid upon going through nvm_validate_semver
TEST_SEMVERS="$TEST_SEMVERS
1.2.3||1.2.4:1.2.3||1.2.4
1.2||1.3:1.2||1.3
1||2:1||2
<1.2.3>:<1.2.3>
<1.2>:<1.2>
<1>:<1>
>>1:>>1
<<1:<<1
==1:==1
**:**
xx:xx
^^1:^^1
~~1:~~1
1.2.3-1.2.4:1.2.3-1.2.4
10.21.32-10.21.33:10.21.32-10.21.33
1.2-1.3:1.2-1.3
10.21-10.22:10.21-10.22
1-2:1-2
10-11:10-11"
# add test semvers with tabs inside them
# These semvers are intended to:
# - validate that semvers can include tabs
# - validate that all tabs or consecutive tabs are reduced to one space
# - validate that leading trailing spaces and tabs are removed
TEST_SEMVERS="$TEST_SEMVERS
1 1 :1 1
1 1 :1 1"
# remove first line which is just an empty line
TEST_SEMVERS=$(echo "$TEST_SEMVERS" | tail -n +2)
# POSITIVE TEST CASES
# (TEST SET #1) uses valid TEST_SEMVER's and valid package.json templates
TEST_SEMVERS_COPY=$(echo "$TEST_SEMVERS")
PREV_TEST_SEMVER=''
for TEMPLATE_NAME in package_json_templates/_valid_*; do
while [ -n "$TEST_SEMVERS_COPY" ]; do
LINE=$(echo "$TEST_SEMVERS_COPY" | head -n1)
TEST_SEMVER_INPUT=$(echo "$LINE" | awk -F: '{ print $1 }')
EXPECTED_OUTPUT=$(echo "$LINE" | awk -F: '{ print $2 }')
TEST_SEMVERS_COPY=$(echo "$TEST_SEMVERS_COPY" | tail -n +2)
[ "$PREV_TEST_SEMVER" = "$TEST_SEMVER_INPUT" ] \
&& die "Problem iterating through TEST_SEMVERS_COPY (TEST SET #1). Encountered the same value twice in a row. PREV_TEST_SEMVER='$PREV_TEST_SEMVER' TEST_SEMVER_INPUT='$TEST_SEMVER_INPUT'.\n"
PREV_TEST_SEMVER=$(printf "%s" "$TEST_SEMVER_INPUT")
PKG_JSON_CONTENTS=$(sed 's/NODE_SEMVER/'"$TEST_SEMVER_INPUT"'/g' "$TEMPLATE_NAME" | tail -n +3)
ACTUAL_OUTPUT=$(nvm_get_node_from_pkg_json "$PKG_JSON_CONTENTS")
[ "$ACTUAL_OUTPUT" = "$EXPECTED_OUTPUT" ] \
&& [ -n "$ACTUAL_OUTPUT" ] \
&& [ -n "$PKG_JSON_CONTENTS" ] \
|| die "'nvm_get_node_from_pkg_json' POSITIVE test case failed (TEST SET #1).
Expected '$EXPECTED_OUTPUT' but got '$ACTUAL_OUTPUT' when given input template '$TEMPLATE_NAME':\n$PKG_JSON_CONTENTS"
done
done
# NEGATIVE TEST CASES
# (TEST SET #2) uses valid TEST_SEMVER's but invalid package.json templates
TEST_SEMVERS_COPY=$(echo "$TEST_SEMVERS")
PREV_TEST_SEMVER=''
for TEMPLATE_NAME in package_json_templates/_invalid_*; do
while [ -n "$TEST_SEMVERS_COPY" ]; do
LINE=$(echo "$TEST_SEMVERS_COPY" | head -n1)
TEST_SEMVER_INPUT=$(echo "$LINE" | awk -F: '{ print $1 }')
TEST_SEMVERS_COPY=$(echo "$TEST_SEMVERS_COPY" | tail -n +2)
[ "$PREV_TEST_SEMVER" = "$TEST_SEMVER_INPUT" ] \
&& die "Problem iterating through TEST_SEMVERS_COPY (TEST SET #2). Encountered the same value twice in a row. PREV_TEST_SEMVER='$PREV_TEST_SEMVER' TEST_SEMVER_INPUT='$TEST_SEMVER_INPUT'.\n"
PREV_TEST_SEMVER=$(printf "%s" "$TEST_SEMVER_INPUT")
PKG_JSON_CONTENTS=$(sed 's/NODE_SEMVER/'"$TEST_SEMVER_INPUT"'/g' "$TEMPLATE_NAME" | tail -n +3)
ACTUAL_OUTPUT=$(nvm_get_node_from_pkg_json "$PKG_JSON_CONTENTS")
[ "$ACTUAL_OUTPUT" = "" ] \
&& [ -n "$TEST_SEMVER_INPUT" ] \
&& [ -n "$PKG_JSON_CONTENTS" ] \
|| die "'nvm_get_node_from_pkg_json' NEGATIVE test case failed (TEST SET #2).
Expected to get empty string but got '$ACTUAL_OUTPUT' when given input template '$TEMPLATE_NAME':\n$PKG_JSON_CONTENTS"
done
done
# invalid test semvers
TEST_SEMVERS='&1
@1
#1
$1
%s
1)
1(
1_
1+
1]
1[
1"
1:
1?
1`
1!'
# (TEST SET #3) uses invalid TEST_SEMVER's but valid package.json templates
TEST_SEMVERS_COPY=$(echo "$TEST_SEMVERS")
PREV_TEST_SEMVER=''
for TEMPLATE_NAME in package_json_templates/_valid_*; do
while [ -n "$TEST_SEMVERS_COPY" ]; do
TEST_SEMVER_INPUT=$(echo "$TEST_SEMVERS_COPY" | head -n1)
TEST_SEMVERS_COPY=$(echo "$TEST_SEMVERS_COPY" | tail -n +2)
[ "$PREV_TEST_SEMVER" = "$TEST_SEMVER_INPUT" ] \
&& die "Problem iterating through TEST_SEMVERS_COPY (TEST SET #3). Encountered the same value twice in a row. PREV_TEST_SEMVER='$PREV_TEST_SEMVER' TEST_SEMVER_INPUT='$TEST_SEMVER_INPUT'.\n"
PREV_TEST_SEMVER=$(printf "%s" "$TEST_SEMVER_INPUT")
PKG_JSON_CONTENTS=$(sed 's/NODE_SEMVER/'"$TEST_SEMVER_INPUT"'/g' "$TEMPLATE_NAME" | tail -n +3)
ACTUAL_OUTPUT=$(nvm_get_node_from_pkg_json "$PKG_JSON_CONTENTS")
[ "$ACTUAL_OUTPUT" = "" ] \
&& [ -n "$TEST_SEMVER_INPUT" ] \
&& [ -n "$PKG_JSON_CONTENTS" ] \
|| die "'nvm_get_node_from_pkg_json' NEGATIVE test case failed (TEST SET #3).
Expected to get empty string but got '$ACTUAL_OUTPUT' when given input template '$TEMPLATE_NAME':\n$PKG_JSON_CONTENTS"
done
done
# (TEST SET #4) uses invalid TEST_SEMVER's and invalid package.json templates
TEST_SEMVERS_COPY=$(echo "$TEST_SEMVERS")
PREV_TEST_SEMVER=''
for TEMPLATE_NAME in package_json_templates/_invalid_*; do
while [ -n "$TEST_SEMVERS_COPY" ]; do
TEST_SEMVER_INPUT=$(echo "$TEST_SEMVERS_COPY" | head -n1)
TEST_SEMVERS_COPY=$(echo "$TEST_SEMVERS_COPY" | tail -n +2)
[ "$PREV_TEST_SEMVER" = "$TEST_SEMVER_INPUT" ] \
&& die "Problem iterating through TEST_SEMVERS_COPY (TEST SET #4). Encountered the same value twice in a row. PREV_TEST_SEMVER='$PREV_TEST_SEMVER' TEST_SEMVER_INPUT='$TEST_SEMVER_INPUT'.\n"
PREV_TEST_SEMVER=$(printf "%s" "$TEST_SEMVER_INPUT")
PKG_JSON_CONTENTS=$(sed 's/NODE_SEMVER/'"$TEST_SEMVER_INPUT"'/g' "$TEMPLATE_NAME" | tail -n +3)
ACTUAL_OUTPUT=$(nvm_get_node_from_pkg_json "$PKG_JSON_CONTENTS")
[ "$ACTUAL_OUTPUT" = "" ] \
&& [ -n "$TEST_SEMVER_INPUT" ] \
&& [ -n "$PKG_JSON_CONTENTS" ] \
|| die "'nvm_get_node_from_pkg_json' NEGATIVE test case failed (TEST SET #4).
Expected to get empty string but got '$ACTUAL_OUTPUT' when given input template '$TEMPLATE_NAME':\n$PKG_JSON_CONTENTS"
done
done

View File

@ -0,0 +1,36 @@
#!/bin/sh
die () { printf "$@" ; exit 1; }
\. ../../../nvm.sh
newest_node=$(nvm_remote_version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+')
x5='5.12.0'
# POSITIVE TEST CASES
# INPUT:EXPECTED_OUTPUT
test_cases="*:$newest_node
5:$x5
x:$newest_node
X:$newest_node
7.1.0 || 7.3.0 || 7.2.0:7.3.0
7.1.0 7.3.0 7.2.0:
5:$x5
5.x:$x5
5.x.x:$x5
5.X:$x5
5.X.X:$x5
7.5.0:7.5.0"
while [ -n "$test_cases" ]; do
LINE=$(echo "$test_cases" | head -n1)
INPUT=$(echo "$LINE" | awk -F: '{ print $1 }')
EXPECTED_OUTPUT=$(echo "$LINE" | awk -F: '{ print $2 }')
ACTUAL_OUTPUT=$(nvm_interpret_complex_semver "$INPUT")
[ "$ACTUAL_OUTPUT" = "$EXPECTED_OUTPUT" ] \
&& [ -n "$INPUT" ] \
|| die "Expected output: '$EXPECTED_OUTPUT'. Actual output: '$ACTUAL_OUTPUT'. Input: '$INPUT'.\n"
test_cases=$(echo "$test_cases" | tail -n +2)
done
exit 0

View File

@ -0,0 +1,42 @@
#!/bin/sh
die () { printf "$@" ; exit 1; }
\. ../../../nvm.sh
valid_semver_regexp='^( ?(<|<=|>|>=|=|~|\^)?([0-9]+|x)\.([0-9]+|x)\.([0-9]+|x))+$'
# POSITIVE TEST CASES
# STRING:REGEXP
test_cases="1.2.3:$valid_semver_regexp
11.22.33:$valid_semver_regexp"
while [ -n "$test_cases" ]; do
LINE=$(echo "$test_cases" | head -n1)
STRING=$(echo "$LINE" | awk -F: '{ print $1 }')
REGEXP=$(echo "$LINE" | awk -F: '{ print $2 }')
[ -n "$REGEXP" ] \
&& [ -n "$STRING" ] \
&& nvm_string_contains_regexp "$STRING" "$REGEXP" \
|| die "nvm_string_contains_regexp POSITIVE test case failed. REGEXP: '$REGEXP'. STRING: '$STRING'.\n"
test_cases=$(echo "$test_cases" | tail -n +2)
done
# NEGATIVE TEST CASES
# STRING:REGEXP
test_cases="1.2.a:$valid_semver_regexp
:$valid_semver_regexp
11.22.a:$valid_semver_regexp"
while [ -n "$test_cases" ]; do
LINE=$(echo "$test_cases" | head -n1)
STRING=$(echo "$LINE" | awk -F: '{ print $1 }')
REGEXP=$(echo "$LINE" | awk -F: '{ print $2 }')
[ -n "$REGEXP" ] \
&& ! nvm_string_contains_regexp "$STRING" "$REGEXP" \
|| die "nvm_string_contains_regexp NEGATIVE test case failed. REGEXP: '$REGEXP'. STRING: '$STRING'.\n"
test_cases=$(echo "$test_cases" | tail -n +2)
done
exit 0

View File

@ -0,0 +1,54 @@
#!/bin/sh
die () { printf "$@" ; exit 1; }
\. ../../../nvm.sh
# INPUT:EXPECTED_OUTPUT
# TODO refactor test cases into clearer structure
test_cases="1.2.3:1.2.3
1.2.3 - 1.2.4:>=1.2.3 <=1.2.4
1.2.3-1.2.4:>=1.2.3 <=1.2.4
11.22.33 - 11.22.44:>=11.22.33 <=11.22.44
1.2.x - 1.2.4:>=1.2.0 <=1.2.4
1.2.xx - 1.2.4:
1.2.3 || 1.2.4:1.2.3 || 1.2.4
*:>0.0.0
x:>0.0.0
X:>0.0.0
1:>=1.0.0 <2.0.0
1.2:>=1.2.0 <1.3.0
< 1.2.3:<1.2.3
> 1.2.3:>1.2.3
<= 1.2.3:<=1.2.3
>= 1.2.3:>=1.2.3
= 1.2.3:1.2.3
^0.0.1 ^0.1.2 ^1.2.3:>=0.0.1 <0.0.2 >=0.1.2 <0.2.0 >=1.2.3 <2.0.0
~0.0.1 ~0.1.2 ~1.2.3:>=0.0.1 <0.1.0 >=0.1.2 <0.2.0 >=1.2.3 <1.3.0
~ 1.2.3:>=1.2.3 <1.3.0
^ 1.2.3:>=1.2.3 <2.0.0
1.2.3 || 1.2.4 1.2.5:1.2.3 || 1.2.4 1.2.5
a:
1 || 2 a:
1 || a:
a || 1.2.3:
1.2.?:
^0.0.1:>=0.0.1 <0.0.2
<x.x.x:
>=x.1.1:>0.0.0
>x.1.1:>0.0.0
~x.1.1:>0.0.0
^x.1.1:>0.0.0
11.22.33:11.22.33"
while [ -n "$test_cases" ]; do
LINE=$(echo "$test_cases" | head -n1)
INPUT=$(echo "$LINE" | awk -F: '{ print $1 }')
EXPECTED_OUTPUT=$(echo "$LINE" | awk -F: '{ print $2 }')
ACTUAL_OUTPUT=$(nvm_validate_semver "$INPUT")
[ "$ACTUAL_OUTPUT" = "$EXPECTED_OUTPUT" ] \
&& [ -n "$INPUT" ] \
|| die "Expected output: '$EXPECTED_OUTPUT'. Actual output: '$ACTUAL_OUTPUT'. Input: '$INPUT'.\n"
test_cases=$(echo "$test_cases" | tail -n +2)
done
exit 0

View File

@ -0,0 +1,6 @@
verifies incompatibility with package.json that is missing 'engines' key
{
"name": "fake",
"description": "fake"
}

View File

@ -0,0 +1,9 @@
verifies incompatibility with package.json that is missing 'engines.node' key
{
"name": "fake",
"engines": {
"npm": "fake"
},
"description": "fake"
}

View File

@ -0,0 +1,9 @@
verifies incompatibility with package.json that is missing quotes around the 'engines'node' value
{
"name": "fake",
"engines": {
"node": NODE_SEMVER
},
"description": "fake"
}

View File

@ -0,0 +1,9 @@
verifies compatibility with basic package.json with keys in the 'engines' object that have brackets
{
"name": "fake",
"engines": {
"potentialFutureKeyWithBracket}": "fake",
"node": "NODE_SEMVER"
}
}

View File

@ -0,0 +1,9 @@
verifies compatibility with basic package.json indented with spaces
{
"name": "fake",
"engines": {
"node": "NODE_SEMVER"
},
"description": "fake"
}

View File

@ -0,0 +1,9 @@
verifies compatibility with basic package.json indented with tabs
{
"name": "fake",
"engines": {
"node": "NODE_SEMVER"
},
"description": "fake"
}

View File

@ -0,0 +1,9 @@
verifies compatibility with basic package.json with tabs in the 'engines.node' value
{
"name": "fake",
"engines": {
"node": " NODE_SEMVER "
},
"description": "fake"
}