Skip to content

Commit eea29bc

Browse files
Add support for configuring npm version in Node devcontainers (#1616)
* feat(node): add npm version selection and installation options * add 'lts' and 'latest' options for npm version selection * feat(node): enhance npm installation with compatibility checks and fallback for incompatible Node.js versions * Update src/node/devcontainer-feature.json Co-authored-by: Copilot <[email protected]> * Update src/node/devcontainer-feature.json Co-authored-by: Copilot <[email protected]> * feat(tests): enhance npm version checks for compatibility and fallback scenarios * Version bump * Version bump * fix(install): update npm version check logic and improve compatibility messaging * fix(install): update npm version check logic and improve compatibility messaging * fix(install): update npm installation loop syntax for clarity --------- Co-authored-by: Copilot <[email protected]>
1 parent 732822d commit eea29bc

7 files changed

Lines changed: 256 additions & 6 deletions

src/node/devcontainer-feature.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"id": "node",
3-
"version": "1.7.1",
3+
"version": "2.0.0",
44
"name": "Node.js (via nvm), yarn and pnpm.",
55
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/node",
66
"description": "Installs Node.js, nvm, yarn, pnpm, and needed dependencies.",
@@ -27,6 +27,21 @@
2727
"default": "/usr/local/share/nvm",
2828
"description": "The path where NVM will be installed."
2929
},
30+
"npmVersion": {
31+
"type": "string",
32+
"proposals": [
33+
"lts",
34+
"latest",
35+
"10.9.0",
36+
"10.8.0",
37+
"10.7.0",
38+
"9.9.3",
39+
"8.19.4",
40+
"none"
41+
],
42+
"default": "none",
43+
"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'."
44+
},
3045
"pnpmVersion": {
3146
"type": "string",
3247
"proposals": [

src/node/install.sh

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# Maintainer: The Dev Container spec maintainers
99

1010
export NODE_VERSION="${VERSION:-"lts"}"
11+
export NPM_VERSION="${NPMVERSION:-"lts"}"
1112
export PNPM_VERSION="${PNPMVERSION:-"latest"}"
1213
export NVM_VERSION="${NVMVERSION:-"latest"}"
1314
export NVM_DIR="${NVMINSTALLPATH:-"/usr/local/share/nvm"}"
@@ -381,6 +382,101 @@ if [ ! -z "${ADDITIONAL_VERSIONS}" ]; then
381382
IFS=$OLDIFS
382383
fi
383384

385+
# Install or update npm to specific version
386+
if [ -z "${NPM_VERSION}" ] || [ "${NPM_VERSION}" = "none" ]; then
387+
echo "Ignoring NPM version update"
388+
elif bash -c ". '${NVM_DIR}/nvm.sh' && type npm >/dev/null 2>&1"; then
389+
(
390+
. "${NVM_DIR}/nvm.sh"
391+
[ ! -z "$http_proxy" ] && npm set proxy="$http_proxy"
392+
[ ! -z "$https_proxy" ] && npm set https-proxy="$https_proxy"
393+
[ ! -z "$no_proxy" ] && npm set noproxy="$no_proxy"
394+
echo "Installing npm version ${NPM_VERSION}..."
395+
396+
CURRENT_NPM_VERSION=$(npm --version 2>/dev/null || echo 'unknown')
397+
echo "Current npm version: $CURRENT_NPM_VERSION"
398+
399+
# Clear npm cache and extract version numbers
400+
npm cache clean --force 2>/dev/null || true
401+
CURRENT_MAJOR=$(echo "$CURRENT_NPM_VERSION" | cut -d. -f1 || echo "0")
402+
NODE_MAJOR=$(node --version 2>/dev/null | cut -d. -f1 | tr -d 'v' || echo "0")
403+
404+
# Dynamically check npm's Node.js requirements and auto-fallback if incompatible
405+
ORIGINAL_NPM_VERSION="$NPM_VERSION"
406+
if [ "$NPM_VERSION" != "none" ]; then
407+
echo "Checking npm compatibility requirements..."
408+
NPM_NODE_REQUIREMENT=$(npm view npm@${NPM_VERSION} engines.node 2>/dev/null || echo "")
409+
410+
if [ -n "$NPM_NODE_REQUIREMENT" ]; then
411+
echo "npm $NPM_VERSION requires Node.js: $NPM_NODE_REQUIREMENT"
412+
413+
# Extract minimum required Node version from requirement string
414+
MIN_NODE=$(echo "$NPM_NODE_REQUIREMENT" | grep -oE '[0-9]+' | head -1 || echo "0")
415+
416+
if [ "$MIN_NODE" -gt "0" ] && [ "$NODE_MAJOR" -lt "$MIN_NODE" ]; then
417+
echo "⚠️ WARNING: npm $NPM_VERSION requires Node.js $MIN_NODE+, you have $NODE_MAJOR.x"
418+
419+
# Find compatible npm version dynamically using same logic
420+
echo "🔍 Finding compatible npm version for Node.js $NODE_MAJOR.x..."
421+
422+
# Try npm major versions in descending order to find highest compatible version
423+
for npm_major in 10 9 8 7 6; do
424+
echo "Checking npm $npm_major compatibility..."
425+
FALLBACK_NODE_REQUIREMENT=$(npm view "npm@${npm_major}" engines.node 2>/dev/null || echo "")
426+
427+
if [ -n "$FALLBACK_NODE_REQUIREMENT" ]; then
428+
MIN_NODE=$(echo "$FALLBACK_NODE_REQUIREMENT" | grep -oE '[0-9]+' | head -1 || echo "0")
429+
430+
if [ "$MIN_NODE" -le "$NODE_MAJOR" ]; then
431+
# Get latest patch version for this compatible major version
432+
NPM_VERSION=$(npm view "npm@${npm_major}" version 2>/dev/null || echo "")
433+
if [ -n "$NPM_VERSION" ]; then
434+
echo "✓ Found compatible npm $NPM_VERSION (requires Node.js $MIN_NODE+)"
435+
echo "🔄 Auto-fallback: Installing compatible npm $NPM_VERSION instead"
436+
break
437+
fi
438+
fi
439+
fi
440+
done
441+
442+
# If no compatible version found, skip npm installation
443+
if [ "$NPM_VERSION" = "$ORIGINAL_NPM_VERSION" ]; then
444+
echo "❌ Could not find compatible npm version, keeping current npm"
445+
NPM_VERSION="none"
446+
fi
447+
elif [ "$MIN_NODE" -gt "0" ]; then
448+
echo "✓ Node.js $NODE_MAJOR.x meets npm $NPM_VERSION requirement"
449+
fi
450+
else
451+
echo "Could not determine Node.js requirements for npm $NPM_VERSION, proceeding anyway..."
452+
fi
453+
fi
454+
455+
# Check if npm installation was cancelled due to compatibility issues
456+
if [ "$NPM_VERSION" = "none" ]; then
457+
echo "Skipping npm installation due to compatibility issues."
458+
else
459+
# Try npm installation with retries
460+
for i in 1 2 3; do
461+
echo "Attempt $i: Running npm install -g npm@$NPM_VERSION"
462+
if npm install -g npm@$NPM_VERSION --force --no-audit --no-fund 2>&1; then
463+
NEW_VERSION=$(npm --version 2>/dev/null || echo 'unknown')
464+
echo "Successfully installed npm@${NPM_VERSION}, new version: $NEW_VERSION"
465+
break
466+
else
467+
echo "Attempt $i failed, retrying..."
468+
sleep 2
469+
if [ $i -eq 3 ]; then
470+
echo "Failed to install npm@${NPM_VERSION} after 3 attempts. Keeping current npm version $(npm --version 2>/dev/null || echo 'unknown')."
471+
fi
472+
fi
473+
done
474+
fi
475+
)
476+
else
477+
echo "Skip installing/updating npm because npm is not available"
478+
fi
479+
384480
# Install pnpm
385481
if [ ! -z "${PNPM_VERSION}" ] && [ "${PNPM_VERSION}" = "none" ]; then
386482
echo "Ignoring installation of PNPM"

test/node/install_npm_latest.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Optional: Import test library
6+
source dev-container-features-test-lib
7+
8+
# When npmVersion="latest", npm should be upgraded from Node.js bundled version if possible
9+
# Node.js 22 comes with npm 10.x, latest should be 11+ if upgrade succeeds
10+
# If upgrade fails, npm should still work (may remain at bundled version)
11+
check "npm_version_upgraded_or_functional" bash -c "
12+
npm --version >/dev/null
13+
NPM_MAJOR=\$(npm --version | cut -d. -f1)
14+
15+
if [ \$NPM_MAJOR -ge 11 ]; then
16+
echo 'npm successfully upgraded to version 11+ (\$NPM_MAJOR.x)'
17+
exit 0
18+
elif [ \$NPM_MAJOR -eq 10 ]; then
19+
echo 'npm upgrade may have failed, but npm 10.x is still functional'
20+
exit 0
21+
else
22+
echo 'npm version \$NPM_MAJOR.x - unexpected version'
23+
exit 1
24+
fi
25+
"
26+
27+
# Also verify pnpm works as configured
28+
check "pnpm_version" bash -c "pnpm -v | grep 8.8.0"
29+
30+
# Report result
31+
reportResults
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/install_npm_none.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Optional: Import test library
6+
source dev-container-features-test-lib
7+
8+
# When npmVersion is "none", npm should not be updated from node's bundled version
9+
check "npm_not_updated" bash -c '
10+
npm --version >/dev/null
11+
12+
NODE_MAJOR=$(node -p "process.versions.node.split(\".\")[0]")
13+
NPM_MAJOR=$(npm --version | cut -d. -f1)
14+
15+
case "$NODE_MAJOR" in
16+
16) EXPECTED_NPM_MAJOR=8 ;;
17+
18|20|22) EXPECTED_NPM_MAJOR=10 ;;
18+
24) EXPECTED_NPM_MAJOR=11 ;;
19+
*)
20+
echo "Unsupported Node major for bundled npm assertion: $NODE_MAJOR"
21+
exit 1
22+
;;
23+
esac
24+
25+
[ "$NPM_MAJOR" = "$EXPECTED_NPM_MAJOR" ]
26+
'
27+
28+
# Report result
29+
reportResults
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Optional: Import test library
6+
source dev-container-features-test-lib
7+
8+
# Verify npm is installed with specific version 10.8.0
9+
check "npm_specific_version" bash -c "npm -v | grep '^10.8.0'"
10+
11+
# Report result
12+
reportResults

test/node/scenarios.json

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@
66
"version": "lts"
77
}
88
}
9-
},
9+
},
1010
"install_node_debian_bookworm": {
1111
"image": "debian:12",
1212
"features": {
1313
"node": {
1414
"version": "lts"
1515
}
1616
}
17-
},
17+
},
1818
"nvm_test_fallback": {
1919
"image": "debian:11",
2020
"features": {
2121
"node": {
2222
"version": "lts"
2323
}
2424
}
25-
},
25+
},
2626
"install_additional_node": {
2727
"image": "debian:11",
2828
"features": {
@@ -98,7 +98,7 @@
9898
"features": {
9999
"node": {
100100
"version": "22",
101-
"pnpmVersion":"8.8.0"
101+
"pnpmVersion": "8.8.0"
102102
}
103103
}
104104
},
@@ -207,5 +207,42 @@
207207
"version": "lts"
208208
}
209209
}
210+
},
211+
"install_specific_npm_version": {
212+
"image": "debian:12",
213+
"features": {
214+
"node": {
215+
"version": "lts",
216+
"npmVersion": "10.8.0"
217+
}
218+
}
219+
},
220+
"install_npm_none": {
221+
"image": "mcr.microsoft.com/devcontainers/base",
222+
"features": {
223+
"node": {
224+
"version": "24",
225+
"npmVersion": "none"
226+
}
227+
}
228+
},
229+
"install_npm_latest": {
230+
"image": "debian:12",
231+
"features": {
232+
"node": {
233+
"version": "22",
234+
"npmVersion": "latest",
235+
"pnpmVersion": "8.8.0"
236+
}
237+
}
238+
},
239+
"install_npm_latest_incompatible": {
240+
"image": "debian:12",
241+
"features": {
242+
"node": {
243+
"version": "16",
244+
"npmVersion": "latest"
245+
}
246+
}
210247
}
211-
}
248+
}

0 commit comments

Comments
 (0)