diff --git a/.editorconfig b/.editorconfig index 32de192..b368348 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,3 +14,10 @@ indent_size = false [Makefile] indent_style = tab + +[test/fast/Unit tests/package_json_templates/*] +indent_style = unset + +[test/fast/Unit tests/nvm_get_node_from_pkg_json] +indent_style = unset +indent_size = unset diff --git a/nvm.sh b/nvm.sh index 8d032c1..31faaee 100644 --- a/nvm.sh +++ b/nvm.sh @@ -328,7 +328,7 @@ nvm_is_valid_semver() { # semver ::= comparator_set ( ' || ' comparator_set )* # comparator_set ::= comparator ( ' ' comparator )* # comparator ::= ( '<' | '<=' | '>' | '>=' | '' ) [0-9]+ '.' [0-9]+ '.' [0-9]+ -# +# # NOTE: Surrounding the function body in parens limits the scope of variables in this function to only this function. nvm_validate_semver() { ( # split the semantic version into comparator_set's @@ -512,7 +512,7 @@ nvm_interpret_complex_semver() { ( is_current_version_compatible=1 # initialized to false # For each comparator in the current_comparator_set_copy: - # - If current_version is compatible with all comparators, we know current_version is the newest compatible version + # - If current_version is compatible with all comparators, we know current_version is the newest compatible version current_comparator_set_copy=$(command printf "%s" "$current_comparator_set" | command tr ' ' '\n') while [ -n "$current_comparator_set_copy" ]; do current_comparator=$(command printf "%s" "$current_comparator_set_copy" | command head -n1 | command sed -E 's/^ +//;s/ +$//') @@ -733,9 +733,9 @@ nvm_get_node_from_pkg_json() { ( if [ "$i" = '"' ]; then if [ "$in_quotes" = 1 ]; then in_quotes=0 - else + else in_quotes=1 - fi + fi # spaces are interpretted as '' here but they need to be retained elif [ "$i" = '' ]; then engines_node_value="$engines_node_value " diff --git a/semver.md b/semver.md index 709d4a1..bb4d68c 100644 --- a/semver.md +++ b/semver.md @@ -1,50 +1,52 @@ -# 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: +# Semantic version interpretation documentation + +Node versions are interpretted from the semver expression located in the engines.node value of the local package.json file. The algorithm for interpretting the semver is expressed at a high level below and is intended to work with the [semantic versioner for npm](https://docs.npmjs.com/misc/semver). + +## 1. Convert the semver into the following grammar: > ``` > 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. - + +## 2. Resolve each comparator set to its newest compatible node version + +**First, if semver only contains a single comparator_set, we may be able to quickly find the newest compatible version.** + +```pseudocode +if semver is looking for an exact match of some specified version and the specified version is a valid node version + resolve to the specified version +else if semver is looking for a version less than or equal to some specified version and the specified version is a valid node version + resolve to the specified version +else if semver is looking for a version greater than or equal to some specified version and the current newest node version is greater than or equal to the specified version + resolve to the current newest node version +else if semver is looking for a version strictly greater than to some specified version and the current newest node version is greater than the specified version + resolve to the current newest node version +else + quick resolution of semver interpretation not possible +``` + +**If quick resolution of semver interpretation does not work, try more complex semver interpretation algorithm.** + +```pseudocode +initialize highest_compatible_versions to an empty list +initialize node_version_list to the list of current remote node versions +for each current_comparator_set in the semver { + for each current_node_version in node_version_list { + for each current_comparator in current_comparator_set { + if current_node_version is compatible with current_comparator + continue seeing if current_node_version might be compatible with all comparators in current_comparator_set + else if current_node_version is not compatible with current_comparator + if it can be determined that no older version will satisfy this comparator, we can move on to the next comparator_set in the semver + else + stop seeing if current_node_version is compatible with all comparators in current_comparator_set and move on to the next version + } + if current_node_version was found to be compatible with all comparators in current_comparator_set + add current_node_version to the highest_compatible_versions list + } +} + +resolve to the highest version among all the versions collected in highest_compatible_versions +``` + diff --git a/test/fast/Unit tests/nvm_get_node_from_pkg_json b/test/fast/Unit tests/nvm_get_node_from_pkg_json index 25526cb..d5cc6ae 100755 --- a/test/fast/Unit tests/nvm_get_node_from_pkg_json +++ b/test/fast/Unit tests/nvm_get_node_from_pkg_json @@ -128,9 +128,9 @@ 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) + LINE=$(echo "$TEST_SEMVERS_COPY" | head -n1) TEST_SEMVER_INPUT=$(echo "$LINE" | awk -F: '{ print $1 }') - EXPECTED_OUTPUT=$(echo "$LINE" | awk -F: '{ print $2 }') + EXPECTED_OUTPUT=$(echo "$LINE" | awk -F: '{ print $2 }') TEST_SEMVERS_COPY=$(echo "$TEST_SEMVERS_COPY" | tail -n +2) [ "$PREV_TEST_SEMVER" = "$TEST_SEMVER_INPUT" ] \ @@ -143,7 +143,7 @@ for TEMPLATE_NAME in package_json_templates/_valid_*; do [ "$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). + || 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 @@ -157,7 +157,7 @@ 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) + 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) @@ -171,7 +171,7 @@ for TEMPLATE_NAME in package_json_templates/_invalid_*; do [ "$ACTUAL_OUTPUT" = "" ] \ && [ -n "$TEST_SEMVER_INPUT" ] \ && [ -n "$PKG_JSON_CONTENTS" ] \ - || die "'nvm_get_node_from_pkg_json' NEGATIVE test case failed (TEST SET #2). + || 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 @@ -213,7 +213,7 @@ for TEMPLATE_NAME in package_json_templates/_valid_*; do [ "$ACTUAL_OUTPUT" = "" ] \ && [ -n "$TEST_SEMVER_INPUT" ] \ && [ -n "$PKG_JSON_CONTENTS" ] \ - || die "'nvm_get_node_from_pkg_json' NEGATIVE test case failed (TEST SET #3). + || 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 @@ -236,7 +236,7 @@ for TEMPLATE_NAME in package_json_templates/_invalid_*; do [ "$ACTUAL_OUTPUT" = "" ] \ && [ -n "$TEST_SEMVER_INPUT" ] \ && [ -n "$PKG_JSON_CONTENTS" ] \ - || die "'nvm_get_node_from_pkg_json' NEGATIVE test case failed (TEST SET #4). + || 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