Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions lib/runtime.func
Original file line number Diff line number Diff line change
Expand Up @@ -1794,25 +1794,44 @@ setup_nodejs() {
# removed without a reinstall behind it, a half-finished global install --
# each leaves a module npm still lists while `command -v` finds nothing, and
# the caller is about to run exactly that binary. So ask the question the
# caller cares about, and put back whatever cannot answer it.
local mod_bin
# caller cares about, and put back whatever cannot answer it. The binaries
# come from the package's own bin field: single-file-cli installs
# `single-file`, @vue/cli installs `vue`.
local mod_pkg mod_bin mod_bins missing still npm_root
npm_root="$(npm root -g 2>/dev/null)"
for mod in "${MODULES[@]}"; do
# Scoped packages do not name their binary (@vue/cli installs `vue`), so
# there is nothing reliable to probe for.
[[ "$mod" == @*/* ]] && continue
mod_bin="${mod%@*}"
[[ -z "$mod_bin" ]] && continue
command -v "$mod_bin" >/dev/null 2>&1 && continue

msg_warn "$mod_bin is installed but not on PATH - reinstalling"
if { $STD npm install -g --force "$mod" 2>/dev/null ||
$STD npm install -g --force "${mod_bin}@latest" 2>/dev/null; } &&
command -v "$mod_bin" >/dev/null 2>&1; then
msg_ok "Restored $mod_bin"
if [[ "$mod" == @* ]]; then
mod_pkg="${mod#@}"
mod_pkg="@${mod_pkg%@*}"
else
msg_error "$mod_bin is not callable and could not be restored"
[[ " ${failed_modules[*]:-} " == *" $mod_bin "* ]] || failed_modules+=("$mod_bin")
mod_pkg="${mod%@*}"
fi
[[ -n "$mod_pkg" ]] || continue
mod_bins=""
[[ -n "$npm_root" && -f "$npm_root/$mod_pkg/package.json" ]] &&
mod_bins="$(node -e 'const p=require(process.argv[1]);const b=p.bin;if(typeof b==="string")console.log(p.name.replace(/^@[^/]+\//,""));else if(b)console.log(Object.keys(b).join(" "))' "$npm_root/$mod_pkg/package.json" 2>/dev/null)"
[[ -z "$mod_bins" && "$mod_pkg" != @*/* ]] && mod_bins="$mod_pkg"
missing=""
for mod_bin in $mod_bins; do
command -v "$mod_bin" >/dev/null 2>&1 || missing+=" $mod_bin"
done
[[ -z "$missing" ]] && continue

msg_warn "${missing# } is installed but not on PATH - reinstalling"
still="$missing"
if $STD npm install -g --force "$mod" 2>/dev/null ||
$STD npm install -g --force "${mod_pkg}@latest" 2>/dev/null; then
still=""
for mod_bin in $missing; do
command -v "$mod_bin" >/dev/null 2>&1 || still+=" $mod_bin"
done
fi
if [[ -z "$still" ]]; then
msg_ok "Restored $mod_pkg"
continue
fi
msg_error "${still# } is not callable and could not be restored"
[[ " ${failed_modules[*]:-} " == *" $mod_pkg "* ]] || failed_modules+=("$mod_pkg")
done

if ((${#failed_modules[@]} > 0)); then
Expand Down
90 changes: 90 additions & 0 deletions tests/nodejs-module-bins.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -uo pipefail

CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
eval "$(awk '/^setup_nodejs[(][)] [{]/,/^}/' "$CORE_DIR/lib/runtime.func")"

TEST_DIR="$(mktemp -d)"
trap 'rm -rf -- "$TEST_DIR"' EXIT
CALLS="$TEST_DIR/calls"
LOG="$TEST_DIR/log"
passed=0 failed=0

export STD=""
msg_info() { echo "info: $*" >>"$LOG"; }
msg_ok() { echo "ok: $*" >>"$LOG"; }
msg_warn() { echo "warn: $*" >>"$LOG"; }
msg_error() { echo "error: $*" >>"$LOG"; }

HAVE="node npm jq pnpm"
command() {
if [[ "${1:-}" == "-v" ]]; then
[[ " $HAVE " == *" $2 "* ]] && { echo "/usr/bin/$2"; return 0; }
return 1
fi
builtin command "$@"
}

is_tool_installed() { echo 22; }
cleanup_legacy_install() { :; }
ensure_apt_working() { :; }
download_gpg_key() { :; }
cache_installed_version() { :; }
_cs_drop_pm_shim() { :; }
_setup_npm() {
# shellcheck disable=SC2034 # read by the extracted setup_nodejs
NPM_SETUP_VERSION="12.2.1"
}
apt() { :; }
apt-get() { :; }
NPM_ROOT="$TEST_DIR/node_modules"
command mkdir -p "$NPM_ROOT/single-file-cli" "$NPM_ROOT/@postlight/parser"
touch "$NPM_ROOT/single-file-cli/package.json" "$NPM_ROOT/@postlight/parser/package.json"
echo "single-file" >"$NPM_ROOT/single-file-cli/bins"
echo "postlight-parser" >"$NPM_ROOT/@postlight/parser/bins"

node() {
case "$1" in
-v) echo v22.23.4 ;;
-e) cat "${3%package.json}bins" ;;
esac
return 0
}
corepack() { :; }
npm() {
case "${1:-}" in
-v) echo "12.2.1" ;;
root) echo "$NPM_ROOT" ;;
list) echo "/usr/lib"; echo "\`-- single-file-cli@2.0.0"; echo "\`-- @postlight/parser@2.2.3" ;;
install) echo "npm $*" >>"$CALLS" ;;
esac
return 0
}
mkdir() { :; }
cd() { [[ "$1" == "/opt" ]] || builtin cd "$@" || return; }

run() {
: >"$CALLS"
: >"$LOG"
NODE_VERSION=22 NODE_MODULE="$1" setup_nodejs
}
expect() {
if [[ "$2" == "$3" ]]; then
echo "PASS $1"
passed=$((passed + 1))
else
echo "FAIL $1: expected <$2>, got <$3>" >&2
failed=$((failed + 1))
fi
}
HAVE="node npm jq single-file postlight-parser"
run "single-file-cli@latest,@postlight/parser@latest"
expect "a binary named apart from its package is found" "" "$(grep -E '^(warn|error):' "$LOG" | grep -i 'path\|callable')"

HAVE="node npm jq postlight-parser"
run "single-file-cli@latest"
expect "a missing binary reinstalls its package" "1" "$(grep -c 'npm install -g --force single-file-cli@latest' "$CALLS")"
expect "the error names the binary and the package" "1" "$(grep -c 'error: single-file is not callable' "$LOG")"

echo "${passed} passed, ${failed} failed"
((failed == 0))
Loading