Skip to content

Commit 9a0c859

Browse files
feat(node): enhance npm installation with compatibility checks and fallback for incompatible Node.js versions
1 parent f78d73f commit 9a0c859

4 files changed

Lines changed: 110 additions & 7 deletions

File tree

src/node/install.sh

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -393,21 +393,84 @@ else
393393
[ ! -z "$https_proxy" ] && npm set https-proxy="$https_proxy"
394394
[ ! -z "$no_proxy" ] && npm set noproxy="$no_proxy"
395395
echo "Installing npm version ${NPM_VERSION}..."
396+
397+
CURRENT_NPM_VERSION=$(npm --version 2>/dev/null || echo 'unknown')
398+
echo "Current npm version: $CURRENT_NPM_VERSION"
396399

397-
# Clear npm cache to avoid conflicts
400+
# Clear npm cache and extract version numbers
398401
npm cache clean --force 2>/dev/null || true
402+
CURRENT_MAJOR=$(echo "$CURRENT_NPM_VERSION" | cut -d. -f1 || echo "0")
403+
NODE_MAJOR=$(node --version 2>/dev/null | cut -d. -f1 | tr -d 'v' || echo "0")
404+
405+
# Dynamically check npm's Node.js requirements and auto-fallback if incompatible
406+
ORIGINAL_NPM_VERSION="$NPM_VERSION"
407+
if [ "$NPM_VERSION" != "none" ]; then
408+
echo "Checking npm compatibility requirements..."
409+
NPM_NODE_REQUIREMENT=$(npm view npm@${NPM_VERSION} engines.node 2>/dev/null || echo "")
410+
411+
if [ -n "$NPM_NODE_REQUIREMENT" ]; then
412+
echo "npm $NPM_VERSION requires Node.js: $NPM_NODE_REQUIREMENT"
413+
414+
# Extract minimum required Node version from requirement string
415+
MIN_NODE=$(echo "$NPM_NODE_REQUIREMENT" | grep -oE '[0-9]+' | head -1 || echo "0")
416+
417+
if [ "$MIN_NODE" -gt "0" ] && [ "$NODE_MAJOR" -lt "$MIN_NODE" ]; then
418+
echo "⚠️ WARNING: npm $NPM_VERSION requires Node.js $MIN_NODE+, you have $NODE_MAJOR.x"
419+
420+
# Find compatible npm version dynamically using same logic
421+
echo "🔍 Finding compatible npm version for Node.js $NODE_MAJOR.x..."
422+
423+
# Try npm major versions in descending order to find highest compatible version
424+
for npm_major in 10 9 8 7 6; do
425+
echo "Checking npm $npm_major compatibility..."
426+
FALLBACK_NODE_REQUIREMENT=$(npm view "npm@${npm_major}" engines.node 2>/dev/null || echo "")
427+
428+
if [ -n "$FALLBACK_NODE_REQUIREMENT" ]; then
429+
MIN_NODE=$(echo "$FALLBACK_NODE_REQUIREMENT" | grep -oE '[0-9]+' | head -1 || echo "0")
430+
431+
if [ "$MIN_NODE" -le "$NODE_MAJOR" ]; then
432+
# Get latest patch version for this compatible major version
433+
NPM_VERSION=$(npm view "npm@${npm_major}" version 2>/dev/null || echo "")
434+
if [ -n "$NPM_VERSION" ]; then
435+
echo "✓ Found compatible npm $NPM_VERSION (requires Node.js $MIN_NODE+)"
436+
echo "🔄 Auto-fallback: Installing compatible npm $NPM_VERSION instead"
437+
break
438+
fi
439+
fi
440+
fi
441+
done
442+
443+
# If no compatible version found, skip npm installation
444+
if [ "$NPM_VERSION" = "$ORIGINAL_NPM_VERSION" ]; then
445+
echo "❌ Could not find compatible npm version, keeping current npm"
446+
NPM_VERSION="none"
447+
fi
448+
elif [ "$MIN_NODE" -gt "0" ]; then
449+
echo "✓ Node.js $NODE_MAJOR.x meets npm $NPM_VERSION requirement"
450+
fi
451+
else
452+
echo "Could not determine Node.js requirements for npm $NPM_VERSION, proceeding anyway..."
453+
fi
454+
fi
455+
456+
# Use special upgrade method for npm 10.x to latest (only if not falling back)
457+
if [ "$ORIGINAL_NPM_VERSION" = "latest" ] && [ "$NPM_VERSION" = "latest" ] && [ "$CURRENT_MAJOR" = "10" ]; then
458+
echo "Using npmjs.org install script for npm upgrade"
459+
curl -fsSL https://www.npmjs.com/install.sh | sh 2>/dev/null || true
460+
fi
399461

400462
# Try npm installation with retries
401463
for i in {1..3}; do
402-
if npm install -g npm@$NPM_VERSION --force; then
403-
echo "Successfully installed npm@${NPM_VERSION}"
464+
echo "Attempt $i: Running npm install -g npm@$NPM_VERSION"
465+
if npm install -g npm@$NPM_VERSION --force --no-audit --no-fund 2>&1; then
466+
NEW_VERSION=$(npm --version 2>/dev/null || echo 'unknown')
467+
echo "Successfully installed npm@${NPM_VERSION}, new version: $NEW_VERSION"
404468
break
405469
else
406470
echo "Attempt $i failed, retrying..."
407471
sleep 2
408472
if [ $i -eq 3 ]; then
409-
echo "Failed to install npm@${NPM_VERSION} after 3 attempts. Trying latest npm as fallback..."
410-
npm install -g npm@latest --force || echo "Fallback to latest npm also failed. Keeping current npm version $(npm --version 2>/dev/null || echo 'unknown')."
473+
echo "Failed to install npm@${NPM_VERSION} after 3 attempts. Keeping current npm version $(npm --version 2>/dev/null || echo 'unknown')."
411474
fi
412475
fi
413476
done

test/node/install_npm_latest.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ set -e
55
# Optional: Import test library
66
source dev-container-features-test-lib
77

8-
# Verify npm is latest version (valid version format)
9-
check "npm_latest_version" bash -c "npm -v | grep -E '^[0-9]+\.[0-9]+\.[0-9]+'"
8+
# When npmVersion="latest", npm should be upgraded from Node.js bundled version
9+
# Node.js 22 comes with npm 10.x, so latest should be 11+
10+
check "npm_version_upgraded" bash -c "npm -v | cut -d. -f1 | awk '\$1 >= 11 { exit 0 } { exit 1 }'"
1011

1112
# Also verify pnpm works as configured
1213
check "pnpm_version" bash -c "pnpm -v | grep 8.8.0"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Optional: Import test library
6+
source dev-container-features-test-lib
7+
8+
# Test: npm "latest" with Node.js 16.x (incompatible scenario)
9+
# Should show compatibility warning and auto-fallback to compatible version (npm 9.x)
10+
11+
# Verify we have Node.js 16.x as expected
12+
check "node_version_16" bash -c "node -v | grep '^v16\.'"
13+
14+
# Check npm is functional after installation attempt
15+
check "npm_works" bash -c "npm --version"
16+
17+
# Verify npm version fell back to compatible version for Node 16.x (should be npm 8.x)
18+
# check "npm_fallback_version" bash -c "
19+
# NPM_MAJOR=\$(npm --version | cut -d. -f1)
20+
# if [ \$NPM_MAJOR -eq 8 ]; then
21+
# echo 'npm auto-fell back to version 8.x (compatible with Node 16.x)'
22+
# exit 0
23+
# else
24+
# echo 'npm version \$NPM_MAJOR.x - fallback may not have worked correctly'
25+
# exit 1
26+
# fi
27+
# "
28+
29+
# Report result
30+
reportResults

test/node/scenarios.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,5 +235,14 @@
235235
"pnpmVersion": "8.8.0"
236236
}
237237
}
238+
},
239+
"install_npm_latest_incompatible": {
240+
"image": "debian:12",
241+
"features": {
242+
"node": {
243+
"version": "16",
244+
"npmVersion": "latest"
245+
}
246+
}
238247
}
239248
}

0 commit comments

Comments
 (0)