From 1db8f7ea8094d35ee9afdf81c6bd3b41d2fced1b Mon Sep 17 00:00:00 2001 From: xxxigm Date: Sun, 14 Jun 2026 17:34:11 +0700 Subject: [PATCH] fix(install): repair existing managed-Node global prefix on re-run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial fix only wrote the prefix npmrc on a fresh Node install, so pre-existing bundled-Node installs (Node already present) were not repaired by re-running the installer — install_node/ensure_node skip when Node is already up to date. Extract the redirect into an idempotent helper (configure_managed_node_npm_prefix / _nb_configure_npm_prefix) that no-ops when there's no Hermes-managed npm, and call it unconditionally from check_node (install.sh) and at the top of ensure_node (node-bootstrap.sh). Re-running the install command now repairs an affected install in place, not just brand-new ones. --- scripts/install.sh | 36 ++++++++++++++------- scripts/lib/node-bootstrap.sh | 24 ++++++++++---- tests/test_install_sh_node_global_prefix.py | 26 ++++++++++++--- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 030d57d4c14..b3b5f104e3d 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -413,6 +413,25 @@ get_command_link_display_dir() { fi } +# Point a Hermes-managed Node's `npm install -g` at a directory that is on +# PATH. npm's default global prefix for a bundled Node is the Node dir itself, +# so global package binaries land in $HERMES_HOME/node/bin — which is NOT on +# PATH (only the command link dir is) and is wiped on every Node upgrade. +# Redirecting the prefix to the link dir's parent makes global bins resolve to +# the command link dir (node/npm/npx live there too, already on PATH) and +# survive upgrades. Scoped to the managed Node via its prefix-local global +# npmrc, so the user's other Node installs and their ~/.npmrc are untouched. +# Hermes's own global installs pass an explicit --prefix and are unaffected. +# Idempotent and a no-op when there is no Hermes-managed npm, so calling it on +# every install run repairs pre-existing installs, not just fresh ones. +configure_managed_node_npm_prefix() { + [ -x "$HERMES_HOME/node/bin/npm" ] || return 0 + local link_dir + link_dir="$(get_command_link_dir)" + mkdir -p "$HERMES_HOME/node/etc" + printf 'prefix=%s\n' "$(dirname "$link_dir")" > "$HERMES_HOME/node/etc/npmrc" +} + get_hermes_command_path() { local link_dir link_dir="$(get_command_link_dir)" @@ -722,6 +741,11 @@ node_satisfies_build() { check_node() { log_info "Checking Node.js (for browser tools)..." + # Repair pre-existing Hermes-managed installs where `npm install -g` lands + # off PATH. No-op when there's no managed Node, so this is safe to run on + # every install — including re-runs that skip the Node (re)install below. + configure_managed_node_npm_prefix + if command -v node &> /dev/null && node_satisfies_build "$(node --version)"; then log_success "Node.js $(node --version) found" HAS_NODE=true @@ -851,17 +875,7 @@ install_node() { ln -sf "$HERMES_HOME/node/bin/npm" "$node_link_dir/npm" ln -sf "$HERMES_HOME/node/bin/npx" "$node_link_dir/npx" - # Point this Node's `npm install -g` at a directory that is actually on - # PATH. By default npm's global prefix is the Node install dir, so user - # globals land in $HERMES_HOME/node/bin — which is NOT on PATH (only the - # link dir is) and is wiped on every Node upgrade. Redirecting the prefix - # to the link dir's parent makes global bins land in the link dir - # (node/npm/npx live there too, and it's already on PATH) and survive - # upgrades. Scoped to this Node via its prefix-local global npmrc, so the - # user's other Node installs and their ~/.npmrc are untouched. Hermes's - # own global installs pass an explicit --prefix and are unaffected. - mkdir -p "$HERMES_HOME/node/etc" - printf 'prefix=%s\n' "$(dirname "$node_link_dir")" > "$HERMES_HOME/node/etc/npmrc" + configure_managed_node_npm_prefix export PATH="$HERMES_HOME/node/bin:$PATH" diff --git a/scripts/lib/node-bootstrap.sh b/scripts/lib/node-bootstrap.sh index 15763d70486..332ad81180a 100644 --- a/scripts/lib/node-bootstrap.sh +++ b/scripts/lib/node-bootstrap.sh @@ -57,6 +57,19 @@ _nb_get_link_dir() { fi } +# Redirect a Hermes-managed Node's `npm install -g` to the command link dir +# (already on PATH) instead of the default $HERMES_HOME/node/bin, which is off +# PATH and wiped on every Node upgrade. Scoped to the managed Node via its +# prefix-local global npmrc; the user's other Node installs / ~/.npmrc are +# untouched. Idempotent no-op when there's no managed npm. +_nb_configure_npm_prefix() { + [ -x "$HERMES_HOME/node/bin/npm" ] || return 0 + local _link_dir + _link_dir="$(_nb_get_link_dir)" + mkdir -p "$HERMES_HOME/node/etc" + printf 'prefix=%s\n' "$(dirname "$_link_dir")" > "$HERMES_HOME/node/etc/npmrc" +} + _nb_node_major() { local v v=$(node --version 2>/dev/null | sed 's/^v//' | cut -d. -f1) @@ -207,12 +220,7 @@ _nb_install_bundled_node() { ln -sf "$HERMES_HOME/node/bin/npm" "$_link_dir/npm" ln -sf "$HERMES_HOME/node/bin/npx" "$_link_dir/npx" - # Redirect this Node's `npm install -g` to the link dir (already on PATH) - # instead of the default $HERMES_HOME/node/bin, which is off PATH and wiped - # on every Node upgrade. Scoped to this Node via its prefix-local global - # npmrc; the user's other Node installs / ~/.npmrc are untouched. - mkdir -p "$HERMES_HOME/node/etc" - printf 'prefix=%s\n' "$(dirname "$_link_dir")" > "$HERMES_HOME/node/etc/npmrc" + _nb_configure_npm_prefix export PATH="$HERMES_HOME/node/bin:$PATH" @@ -228,6 +236,10 @@ _nb_install_bundled_node() { ensure_node() { HERMES_NODE_AVAILABLE=false + # Repair pre-existing managed installs where `npm install -g` lands off + # PATH. No-op when there's no managed Node, so it's safe to run first. + _nb_configure_npm_prefix + if _nb_have_modern_node; then _nb_ok "Node $(node --version) found" HERMES_NODE_AVAILABLE=true diff --git a/tests/test_install_sh_node_global_prefix.py b/tests/test_install_sh_node_global_prefix.py index f604fc97d77..e43b9201bd1 100644 --- a/tests/test_install_sh_node_global_prefix.py +++ b/tests/test_install_sh_node_global_prefix.py @@ -25,15 +25,31 @@ def test_install_sh_redirects_bundled_npm_global_prefix_to_link_dir() -> None: # The redirect must target the link dir's PARENT so global bins resolve to # /bin == the command link dir (node/npm/npx live there and it is # guaranteed on PATH by the installer's PATH setup). - assert 'printf \'prefix=%s\\n\' "$(dirname "$node_link_dir")" > "$HERMES_HOME/node/etc/npmrc"' in text + assert "configure_managed_node_npm_prefix()" in text + assert 'printf \'prefix=%s\\n\' "$(dirname "$link_dir")" > "$HERMES_HOME/node/etc/npmrc"' in text - # The npmrc lives under the bundled Node so it only affects this npm, not - # the user's other Node installs or their ~/.npmrc. - assert '"$HERMES_HOME/node/etc/npmrc"' in text + +def test_install_sh_repairs_existing_managed_node_on_rerun() -> None: + """The redirect must run on every install (not just fresh Node installs), + so re-running the installer repairs pre-existing managed installs whose + Node is already up to date and would otherwise skip install_node.""" + text = INSTALL_SH.read_text() + + check_node_body = text.split("check_node()", 1)[1].split("\ninstall_node()", 1)[0] + assert "configure_managed_node_npm_prefix" in check_node_body + + # No-op guard so it's safe to call when there is no managed Node. + assert '[ -x "$HERMES_HOME/node/bin/npm" ] || return 0' in text def test_node_bootstrap_redirects_bundled_npm_global_prefix_to_link_dir() -> None: text = NODE_BOOTSTRAP.read_text() + assert "_nb_configure_npm_prefix()" in text assert 'printf \'prefix=%s\\n\' "$(dirname "$_link_dir")" > "$HERMES_HOME/node/etc/npmrc"' in text - assert '"$HERMES_HOME/node/etc/npmrc"' in text + + # Runs at the top of ensure_node so existing managed installs are repaired + # even when a modern Node is already present (early return path). + ensure_node_body = text.split("ensure_node()", 1)[1] + assert "_nb_configure_npm_prefix" in ensure_node_body + assert '[ -x "$HERMES_HOME/node/bin/npm" ] || return 0' in text