Skip to content
Open
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
17 changes: 16 additions & 1 deletion src/node/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "node",
"version": "1.7.1",
"version": "2.0.0",
"name": "Node.js (via nvm), yarn and pnpm.",
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/node",
"description": "Installs Node.js, nvm, yarn, pnpm, and needed dependencies.",
Expand All @@ -27,6 +27,21 @@
"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",
"none"
],
"default": "none",
"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
97 changes: 97 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,102 @@ 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

if [ -z "$NPM_VERSION" ] || [ "$NPM_VERSION" = "none" ]; then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please move these to the top condition? That would simplify the branching here a lot.

echo "Skipping npm installation because NPM_VERSION is '${NPM_VERSION:-empty}'."
else
# 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
fi
)
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
31 changes: 31 additions & 0 deletions test/node/install_npm_latest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/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 if possible
# Node.js 22 comes with npm 10.x, latest should be 11+ if upgrade succeeds
# If upgrade fails, npm should still work (may remain at bundled version)
check "npm_version_upgraded_or_functional" bash -c "
npm --version >/dev/null
NPM_MAJOR=\$(npm --version | cut -d. -f1)

if [ \$NPM_MAJOR -ge 11 ]; then
echo 'npm successfully upgraded to version 11+ (\$NPM_MAJOR.x)'
exit 0
elif [ \$NPM_MAJOR -eq 10 ]; then
echo 'npm upgrade may have failed, but npm 10.x is still functional'
exit 0
else
echo 'npm version \$NPM_MAJOR.x - unexpected version'
exit 1
fi
"

# 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
29 changes: 29 additions & 0 deletions test/node/install_npm_none.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/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 >/dev/null

NODE_MAJOR=$(node -p "process.versions.node.split(\".\")[0]")
NPM_MAJOR=$(npm --version | cut -d. -f1)

case "$NODE_MAJOR" in
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we can control which Node version is installed, can we pin to the latest (24) -- so that upgrades don't break this test, and then verify NPM 11 is installed?

16) EXPECTED_NPM_MAJOR=8 ;;
18|20|22) EXPECTED_NPM_MAJOR=10 ;;
24) EXPECTED_NPM_MAJOR=11 ;;
*)
echo "Unsupported Node major for bundled npm assertion: $NODE_MAJOR"
exit 1
;;
esac

[ "$NPM_MAJOR" = "$EXPECTED_NPM_MAJOR" ]
'

# 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