Skip to content
Draft
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
18 changes: 17 additions & 1 deletion src/node/devcontainer-feature.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@
"default": "/usr/local/share/nvm",
"description": "The path where NVM will be installed."
},
"npmVersion": {
"type": "string",
"proposals": [
"lts",
"latest",
"10.9.0",
"10.8.0",
"10.7.0",
"9.9.3",
"8.19.4",
"latest",
"none"
],
"default": "10.9.0",
"description": "Select or enter a specific NPM version to install globally. Use 'latest' for the latest version, 'none' to skip npm version update, or specify a version like '10.9.0'."
},
"pnpmVersion": {
"type": "string",
"proposals": [
Expand Down Expand Up @@ -78,4 +94,4 @@
"installsAfter": [
"ghcr.io/devcontainers/features/common-utils"
]
}
}
99 changes: 99 additions & 0 deletions src/node/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# Maintainer: The Dev Container spec maintainers

export NODE_VERSION="${VERSION:-"lts"}"
export NPM_VERSION="${NPMVERSION:-"lts"}"
export PNPM_VERSION="${PNPMVERSION:-"latest"}"
export NVM_VERSION="${NVMVERSION:-"latest"}"
export NVM_DIR="${NVMINSTALLPATH:-"/usr/local/share/nvm"}"
Expand Down Expand Up @@ -381,6 +382,104 @@ if [ ! -z "${ADDITIONAL_VERSIONS}" ]; then
IFS=$OLDIFS
fi

# Install or update npm to specific version
if [ ! -z "${NPM_VERSION}" ] && [ "${NPM_VERSION}" = "none" ]; then
echo "Ignoring NPM version update"
else
if bash -c ". '${NVM_DIR}/nvm.sh' && type npm >/dev/null 2>&1"; then
(
. "${NVM_DIR}/nvm.sh"
[ ! -z "$http_proxy" ] && npm set proxy="$http_proxy"
[ ! -z "$https_proxy" ] && npm set https-proxy="$https_proxy"
[ ! -z "$no_proxy" ] && npm set noproxy="$no_proxy"
echo "Installing npm version ${NPM_VERSION}..."

CURRENT_NPM_VERSION=$(npm --version 2>/dev/null || echo 'unknown')
echo "Current npm version: $CURRENT_NPM_VERSION"

# Clear npm cache and extract version numbers
npm cache clean --force 2>/dev/null || true
CURRENT_MAJOR=$(echo "$CURRENT_NPM_VERSION" | cut -d. -f1 || echo "0")
NODE_MAJOR=$(node --version 2>/dev/null | cut -d. -f1 | tr -d 'v' || echo "0")

# Dynamically check npm's Node.js requirements and auto-fallback if incompatible
ORIGINAL_NPM_VERSION="$NPM_VERSION"
if [ "$NPM_VERSION" != "none" ]; then
echo "Checking npm compatibility requirements..."
NPM_NODE_REQUIREMENT=$(npm view npm@${NPM_VERSION} engines.node 2>/dev/null || echo "")

if [ -n "$NPM_NODE_REQUIREMENT" ]; then
echo "npm $NPM_VERSION requires Node.js: $NPM_NODE_REQUIREMENT"

# Extract minimum required Node version from requirement string
MIN_NODE=$(echo "$NPM_NODE_REQUIREMENT" | grep -oE '[0-9]+' | head -1 || echo "0")

if [ "$MIN_NODE" -gt "0" ] && [ "$NODE_MAJOR" -lt "$MIN_NODE" ]; then
echo "⚠️ WARNING: npm $NPM_VERSION requires Node.js $MIN_NODE+, you have $NODE_MAJOR.x"

# Find compatible npm version dynamically using same logic
echo "🔍 Finding compatible npm version for Node.js $NODE_MAJOR.x..."

# Try npm major versions in descending order to find highest compatible version
for npm_major in 10 9 8 7 6; do
echo "Checking npm $npm_major compatibility..."
FALLBACK_NODE_REQUIREMENT=$(npm view "npm@${npm_major}" engines.node 2>/dev/null || echo "")

if [ -n "$FALLBACK_NODE_REQUIREMENT" ]; then
MIN_NODE=$(echo "$FALLBACK_NODE_REQUIREMENT" | grep -oE '[0-9]+' | head -1 || echo "0")

if [ "$MIN_NODE" -le "$NODE_MAJOR" ]; then
# Get latest patch version for this compatible major version
NPM_VERSION=$(npm view "npm@${npm_major}" version 2>/dev/null || echo "")
if [ -n "$NPM_VERSION" ]; then
echo "✓ Found compatible npm $NPM_VERSION (requires Node.js $MIN_NODE+)"
echo "🔄 Auto-fallback: Installing compatible npm $NPM_VERSION instead"
break
fi
fi
fi
done

# If no compatible version found, skip npm installation
if [ "$NPM_VERSION" = "$ORIGINAL_NPM_VERSION" ]; then
echo "❌ Could not find compatible npm version, keeping current npm"
NPM_VERSION="none"
fi
elif [ "$MIN_NODE" -gt "0" ]; then
echo "✓ Node.js $NODE_MAJOR.x meets npm $NPM_VERSION requirement"
fi
else
echo "Could not determine Node.js requirements for npm $NPM_VERSION, proceeding anyway..."
fi
fi

# Use special upgrade method for npm 10.x to latest (only if not falling back)
if [ "$ORIGINAL_NPM_VERSION" = "latest" ] && [ "$NPM_VERSION" = "latest" ] && [ "$CURRENT_MAJOR" = "10" ]; then
echo "Using npmjs.org install script for npm upgrade"
curl -fsSL https://www.npmjs.com/install.sh | sh 2>/dev/null || true
fi

# Try npm installation with retries
for i in {1..3}; do
echo "Attempt $i: Running npm install -g npm@$NPM_VERSION"
if npm install -g npm@$NPM_VERSION --force --no-audit --no-fund 2>&1; then
NEW_VERSION=$(npm --version 2>/dev/null || echo 'unknown')
echo "Successfully installed npm@${NPM_VERSION}, new version: $NEW_VERSION"
break
else
echo "Attempt $i failed, retrying..."
sleep 2
if [ $i -eq 3 ]; then
echo "Failed to install npm@${NPM_VERSION} after 3 attempts. Keeping current npm version $(npm --version 2>/dev/null || echo 'unknown')."
fi
fi
done
)
else
echo "Skip installing/updating npm because npm is not available"
fi
fi

# Install pnpm
if [ ! -z "${PNPM_VERSION}" ] && [ "${PNPM_VERSION}" = "none" ]; then
echo "Ignoring installation of PNPM"
Expand Down
16 changes: 16 additions & 0 deletions test/node/install_npm_latest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# When npmVersion="latest", npm should be upgraded from Node.js bundled version
# Node.js 22 comes with npm 10.x, so latest should be 11+
check "npm_version_upgraded" bash -c "npm -v | cut -d. -f1 | awk '\$1 >= 11 { exit 0 } { exit 1 }'"

# Also verify pnpm works as configured
check "pnpm_version" bash -c "pnpm -v | grep 8.8.0"

# Report result
reportResults
30 changes: 30 additions & 0 deletions test/node/install_npm_latest_incompatible.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# Test: npm "latest" with Node.js 16.x (incompatible scenario)
# Should show compatibility warning and auto-fallback to compatible version (npm 9.x)

# Verify we have Node.js 16.x as expected
check "node_version_16" bash -c "node -v | grep '^v16\.'"

# Check npm is functional after installation attempt
check "npm_works" bash -c "npm --version"

# Verify npm version fell back to compatible version for Node 16.x (should be npm 8.x)
# check "npm_fallback_version" bash -c "
# NPM_MAJOR=\$(npm --version | cut -d. -f1)
# if [ \$NPM_MAJOR -eq 8 ]; then
# echo 'npm auto-fell back to version 8.x (compatible with Node 16.x)'
# exit 0
# else
# echo 'npm version \$NPM_MAJOR.x - fallback may not have worked correctly'
# exit 1
# fi
# "

# Report result
reportResults
12 changes: 12 additions & 0 deletions test/node/install_npm_none.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# When npmVersion is "none", npm should not be updated from node's bundled version
check "npm_not_updated" bash -c "npm --version"

# Report result
reportResults
12 changes: 12 additions & 0 deletions test/node/install_specific_npm_version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# Verify npm is installed with specific version 10.8.0
check "npm_specific_version" bash -c "npm -v | grep '^10.8.0'"

# Report result
reportResults
47 changes: 42 additions & 5 deletions test/node/scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@
"version": "lts"
}
}
},
},
"install_node_debian_bookworm": {
"image": "debian:12",
"features": {
"node": {
"version": "lts"
}
}
},
},
"nvm_test_fallback": {
"image": "debian:11",
"features": {
"node": {
"version": "lts"
}
}
},
},
"install_additional_node": {
"image": "debian:11",
"features": {
Expand Down Expand Up @@ -98,7 +98,7 @@
"features": {
"node": {
"version": "22",
"pnpmVersion":"8.8.0"
"pnpmVersion": "8.8.0"
}
}
},
Expand Down Expand Up @@ -207,5 +207,42 @@
"version": "lts"
}
}
},
"install_specific_npm_version": {
"image": "debian:12",
"features": {
"node": {
"version": "lts",
"npmVersion": "10.8.0"
}
}
},
"install_npm_none": {
"image": "mcr.microsoft.com/devcontainers/base",
"features": {
"node": {
"version": "lts",
"npmVersion": "none"
}
}
},
"install_npm_latest": {
"image": "debian:12",
"features": {
"node": {
"version": "22",
"npmVersion": "latest",
"pnpmVersion": "8.8.0"
}
}
},
"install_npm_latest_incompatible": {
"image": "debian:12",
"features": {
"node": {
"version": "16",
"npmVersion": "latest"
}
}
}
}
}
Loading