@@ -14,6 +14,49 @@ RESOLVE="${BP_DIR}/lib/vendor/resolve-version-$(get_os)"
1414function runtimes::nodejs::install() {
1515 local requested_version=" ${1:- } "
1616 local dir=" ${2:? } "
17+
18+ local log_file
19+ log_file=$( mktemp)
20+
21+ local start
22+ start=$( build_data::current_unix_realtime)
23+
24+ # Run inside `if !` so errexit is suppressed and we can inspect the failure ourselves.
25+ # Capture only stdout into the log: `_install` echoes its failure discriminators to stdout
26+ # for the classifier, while user-facing warnings/errors go to stderr (via output::*) and must
27+ # pass straight through to the real stderr — so we deliberately do NOT merge `2>&1` here.
28+ # `tee` passes stdout through, so normal install output still reaches the caller's pipe.
29+ # shellcheck disable=SC2310 # invoked in a condition so set -e is disabled inside
30+ if ! { runtimes::nodejs::_install " ${requested_version} " " ${dir} " | tee " ${log_file} " ; }; then
31+ # Capture the full pipe status before any other command clobbers PIPESTATUS.
32+ local install_exit=" ${PIPESTATUS[0]} "
33+ build_data::set_duration " install_node_binary_time" " ${start} "
34+
35+ local -A failure
36+ # shellcheck disable=SC2310 # the classifier fills `failure` by nameref; invoked directly so its writes survive
37+ if runtimes::nodejs::_handle_install_failure " ${log_file} " " ${requested_version} " failure; then
38+ failure::emit failure
39+ fi
40+
41+ # No known failure mode recognised. Bubble up so the legacy ERR trap classifies it
42+ # (generic fallback) while matchers not yet migrated here still get handled.
43+ return " ${install_exit} "
44+ fi
45+
46+ build_data::set_duration " install_node_binary_time" " ${start} "
47+
48+ local node_version node_version_major bundled_npm_version
49+ node_version=$( node --version)
50+ node_version_major=$( get_node_major_version)
51+ bundled_npm_version=$( npm --version)
52+ build_data::set_string " node_version" " ${node_version} "
53+ build_data::set_raw " node_version_major" " ${node_version_major} "
54+ build_data::set_string " bundled_npm_version" " ${bundled_npm_version} "
55+ }
56+
57+ function runtimes::nodejs::_install() {
58+ local requested_version=" ${1:- } "
59+ local dir=" ${2:? } "
1760 local resolve_result
1861
1962 if [[ -n " ${NODE_BINARY_URL} " ]]; then
@@ -26,11 +69,22 @@ function runtimes::nodejs::install() {
2669 echo " Resolving node version ${requested_version} ..."
2770 fi
2871
29- # shellcheck disable=SC2310 # || disables errexit, but that's intentional for this pattern
30- resolve_result=$( runtimes::nodejs::_resolve " ${requested_version} " || echo " failed" )
72+ # The resolver prints "No result" on stdout (exit 0) for an unknown version, or
73+ # "Could not parse"/"Could not get …" for a malformed requirement; capture stderr too so
74+ # either surfaces in a single invocation.
75+ local resolve_result resolve_exit_code
76+ resolve_result=$( " ${RESOLVE} " " ${BP_DIR} /inventory/node.toml" " ${requested_version} " 2>&1 ) && resolve_exit_code=0 || resolve_exit_code=$?
3177
32- if [[ " ${resolve_result} " == " failed" ]]; then
33- fail_bin_install " ${requested_version} "
78+ if [[ " ${resolve_exit_code} " -ne 0 || " ${resolve_result} " == " No result" ]]; then
79+ # Print the canonical discriminating line so _handle_install_failure can classify it.
80+ if [[ " ${resolve_result} " == " No result" ]]; then
81+ echo " Could not find Node version corresponding to version requirement: ${requested_version} "
82+ elif [[ " ${resolve_result} " == " Could not parse" * ]] || [[ " ${resolve_result} " == " Could not get" * ]]; then
83+ echo " Error: Invalid semantic version \" ${requested_version} \" "
84+ else
85+ echo " Error: Unknown error installing \" ${requested_version} \" of node"
86+ fi
87+ return 1
3488 fi
3589
3690 version=$( echo " ${resolve_result} " | jq -r .version)
@@ -71,7 +125,7 @@ function runtimes::nodejs::install() {
71125 If that doesn't help, check the Node.js status page:
72126 https://status.nodejs.org/
73127 EOF
74- false
128+ return 1
75129 fi
76130
77131 if [[ -z " ${NODE_BINARY_URL} " ]]; then
@@ -83,15 +137,15 @@ function runtimes::nodejs::install() {
83137 output::error << -EOF
84138 Checksum validation failed for Node.js ${version} - ${checksum_type} :${checksum_value}
85139 EOF
86- false
140+ return 1
87141 fi
88142 ;;
89143 * )
90144 build_data::set_string " failure" " unsupported-checksum"
91145 output::error << -EOF
92146 Unsupported checksum for Node.js ${version} - ${checksum_type} :${checksum_value}
93147 EOF
94- false
148+ return 1
95149 ;;
96150 esac
97151 fi
@@ -101,21 +155,6 @@ function runtimes::nodejs::install() {
101155 chmod +x " ${dir} " /bin/*
102156}
103157
104- function runtimes::nodejs::_resolve() {
105- local node_version=" $1 "
106- local output
107-
108- if output=$( " ${RESOLVE} " " ${BP_DIR} /inventory/node.toml" " ${node_version} " ) ; then
109- if [[ ${output} = " No result" ]]; then
110- return 1
111- else
112- echo " ${output} "
113- return 0
114- fi
115- fi
116- return 1
117- }
118-
119158function runtimes::nodejs::_warn_wide_range() {
120159 local requested_version=" $1 "
121160 local lts_version=" $2 "
@@ -164,13 +203,70 @@ function runtimes::nodejs::_warn_known_bad_release() {
164203
165204# Pure classifier for Node.js install failures.
166205#
167- # Input: $1 path to the captured Node install log; $2 name of an associative array to fill.
206+ # Input: $1 path to the captured Node install log; $2 the requested version requirement;
207+ # $3 name of an associative array to fill.
168208# Returns 0 + fills the array on a known failure; returns 1 untouched otherwise. No side effects.
169209function runtimes::nodejs::_handle_install_failure() {
170210 local log_file=" ${1} "
211+ local requested_version=" ${2} "
171212 # shellcheck disable=SC2178 # nameref alias to the caller's associative array, not a string
172- local -n __failure=" ${2} "
213+ local -n __failure=" ${3} "
214+
215+ if grep -qi ' Could not find Node version corresponding to version requirement' " ${log_file} " ; then
216+ __failure[" id" ]=" invalid-node-version"
217+ __failure[" classification" ]=" user"
218+ __failure[" detail" ]=" ${requested_version} "
219+ __failure[" message" ]=$(
220+ cat << -EOF
221+ No matching version found for Node: ${requested_version}
222+
223+ Heroku supports the latest Stable version of Node.js as well as all
224+ active LTS (Long-Term-Support) versions, however you have specified
225+ a version in package.json (${requested_version} ) that does not correspond to
226+ any published version of Node.js.
227+
228+ You should always specify a Node.js version that matches the runtime
229+ you're developing and testing with. To find your version locally:
230+
231+ $ node --version
232+ v6.11.1
233+
234+ Use the engines section of your package.json to specify the version of
235+ Node.js to use on Heroku. Drop the 'v' to save only the version number:
236+
237+ "engines": {
238+ "node": "6.11.1"
239+ }
240+
241+ https://help.heroku.com/6235QYN4/
242+ EOF
243+ )
244+ return 0
245+ fi
246+
247+ if grep -qi ' Error: Invalid semantic version' " ${log_file} " ; then
248+ __failure[" id" ]=" invalid-semver-requirement"
249+ __failure[" classification" ]=" user"
250+ __failure[" detail" ]=" ${requested_version} "
251+ __failure[" message" ]=$(
252+ cat << -EOF
253+ Invalid semver requirement
254+
255+ Node, Yarn, and npm adhere to semver, the semantic versioning convention
256+ popularized by GitHub.
257+
258+ http://semver.org/
259+
260+ However you have specified a version requirement that is not a valid
261+ semantic version.
262+
263+ https://help.heroku.com/0ZIOF3ST
264+ EOF
265+ )
266+ return 0
267+ fi
173268
269+ # No known failure mode recognised — let the caller fall through.
174270 return 1
175271}
176272
0 commit comments