Skip to content

Commit 5a28ecb

Browse files
committed
[New] nvm install --offline: install from cache without network access
Add `--offline` flag to `nvm install` that resolves versions using only locally installed versions and cached downloads. No network calls are made. New helper functions `nvm_ls_cached` and `nvm_offline_version` scan `$NVM_DIR/.cache/bin/` for previously downloaded tarballs. In offline mode, `nvm_download_artifact` returns cached tarballs directly without checksum verification or download attempts. The curl/wget requirement is skipped when `--offline` is set. Supports `--lts` via locally stored LTS alias files.
1 parent a5d9d98 commit 5a28ecb

4 files changed

Lines changed: 208 additions & 17 deletions

File tree

‎README.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,22 @@ nvm install-latest-npm
482482
If you've already gotten an error to the effect of "npm does not support Node.js", you'll need to (1) revert to a previous node version (`nvm ls` & `nvm use <your latest _working_ version from the ls>`), (2) delete the newly created node version (`nvm uninstall <your _broken_ version of node from the ls>`), then (3) rerun your `nvm install` with the `--latest-npm` flag.
483483

484484

485+
### Offline Install
486+
487+
If you've previously downloaded a node version (or it's still in the cache), you can install it without any network access using the `--offline` flag:
488+
489+
```sh
490+
nvm install --offline 14.7.0
491+
```
492+
493+
This resolves versions using only locally installed versions and cached downloads. It will not attempt to download anything. This is useful in air-gapped environments, on planes, or when you want to avoid network latency.
494+
495+
You can combine `--offline` with `--lts` to install the latest cached LTS version (as long as LTS aliases have been populated by a prior `nvm ls-remote --lts`):
496+
497+
```sh
498+
nvm install --offline --lts
499+
```
500+
485501
### Default Global Packages From File While Installing
486502

487503
If you have a list of default packages you want installed every time you install a new version, we support that too -- just add the package names, one per line, to the file `$NVM_DIR/default-packages`. You can add anything npm would accept as a package argument on the command line.

‎nvm.sh‎

Lines changed: 115 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2490,22 +2490,35 @@ nvm_download_artifact() {
24902490
local COMPRESSION
24912491
COMPRESSION="$(nvm_get_artifact_compression "${VERSION}")"
24922492

2493-
local CHECKSUM
2494-
CHECKSUM="$(nvm_get_checksum "${FLAVOR}" "${TYPE}" "${VERSION}" "${SLUG}" "${COMPRESSION}")"
2495-
24962493
local tmpdir
24972494
if [ "${KIND}" = 'binary' ]; then
24982495
tmpdir="$(nvm_cache_dir)/bin/${SLUG}"
24992496
else
25002497
tmpdir="$(nvm_cache_dir)/src/${SLUG}"
25012498
fi
2499+
2500+
local TARBALL
2501+
TARBALL="${tmpdir}/${SLUG}.${COMPRESSION}"
2502+
2503+
if [ "${NVM_OFFLINE-}" = 1 ]; then
2504+
# In offline mode, use cached tarball without checksum or download
2505+
if [ -r "${TARBALL}" ]; then
2506+
nvm_err "Offline: using cached archive $(nvm_sanitize_path "${TARBALL}")"
2507+
nvm_echo "${TARBALL}"
2508+
return 0
2509+
fi
2510+
nvm_err "Offline: no cached archive found for ${SLUG}"
2511+
return 4
2512+
fi
2513+
2514+
local CHECKSUM
2515+
CHECKSUM="$(nvm_get_checksum "${FLAVOR}" "${TYPE}" "${VERSION}" "${SLUG}" "${COMPRESSION}")"
2516+
25022517
command mkdir -p "${tmpdir}/files" || (
25032518
nvm_err "creating directory ${tmpdir}/files failed"
25042519
return 3
25052520
)
25062521

2507-
local TARBALL
2508-
TARBALL="${tmpdir}/${SLUG}.${COMPRESSION}"
25092522
local TARBALL_URL
25102523
if nvm_version_greater_than_or_equal_to "${VERSION}" 0.1.14; then
25112524
TARBALL_URL="${MIRROR}/${VERSION}/${SLUG}.${COMPRESSION}"
@@ -3050,6 +3063,57 @@ nvm_cache_dir() {
30503063
nvm_echo "${NVM_DIR}/.cache"
30513064
}
30523065

3066+
# args: pattern
3067+
# Lists versions available in the local cache (not yet installed).
3068+
# Returns version numbers like "v18.20.4", one per line, sorted.
3069+
nvm_ls_cached() {
3070+
local PATTERN
3071+
PATTERN="${1-}"
3072+
local CACHE_BIN_DIR
3073+
CACHE_BIN_DIR="$(nvm_cache_dir)/bin"
3074+
if [ ! -d "${CACHE_BIN_DIR}" ]; then
3075+
return
3076+
fi
3077+
local NVM_OS
3078+
NVM_OS="$(nvm_get_os)"
3079+
local NVM_ARCH
3080+
NVM_ARCH="$(nvm_get_arch)"
3081+
local SUFFIX
3082+
SUFFIX="${NVM_OS}-${NVM_ARCH}"
3083+
# shellcheck disable=SC2010
3084+
command ls -1 "${CACHE_BIN_DIR}" \
3085+
| nvm_grep "^node-v.*-${SUFFIX}\$" \
3086+
| command sed "s/^node-\\(v[0-9][0-9.]*\\)-${SUFFIX}\$/\\1/" \
3087+
| nvm_grep "$(nvm_ensure_version_prefix "${PATTERN}")" \
3088+
| command sort -t. -u -k 1.2,1n -k 2,2n -k 3,3n
3089+
}
3090+
3091+
# args: pattern
3092+
# Resolves a version pattern to a single version using only locally
3093+
# installed versions and cached downloads. No network access.
3094+
nvm_offline_version() {
3095+
local PATTERN
3096+
PATTERN="${1-}"
3097+
3098+
# First try locally installed versions
3099+
local VERSION
3100+
VERSION="$(nvm_version "${PATTERN}")"
3101+
if [ "_${VERSION}" != '_N/A' ]; then
3102+
nvm_echo "${VERSION}"
3103+
return 0
3104+
fi
3105+
3106+
# Then try cached downloads
3107+
VERSION="$(nvm_ls_cached "${PATTERN}" | command tail -1)"
3108+
if [ -n "${VERSION}" ]; then
3109+
nvm_echo "${VERSION}"
3110+
return 0
3111+
fi
3112+
3113+
nvm_echo 'N/A'
3114+
return 3
3115+
}
3116+
30533117
nvm() {
30543118
if [ "$#" -lt 1 ]; then
30553119
nvm --help
@@ -3130,6 +3194,7 @@ nvm() {
31303194
nvm_echo ' --skip-default-packages When installing, skip the default-packages file if it exists'
31313195
nvm_echo ' --latest-npm After installing, attempt to upgrade to the latest working npm on the given node version'
31323196
nvm_echo ' --no-progress Disable the progress bar on any downloads'
3197+
nvm_echo ' --offline Install from cache only, without downloading anything'
31333198
nvm_echo ' --alias=<name> After installing, set the alias specified to the version specified. (same as: nvm alias <name> <version>)'
31343199
nvm_echo ' --default After installing, set default alias to the version specified. (same as: nvm alias default <version>)'
31353200
nvm_echo ' --save After installing, write the specified version to .nvmrc'
@@ -3335,21 +3400,18 @@ nvm() {
33353400
local NVM_OS
33363401
NVM_OS="$(nvm_get_os)"
33373402

3338-
if ! nvm_has "curl" && ! nvm_has "wget"; then
3339-
nvm_err 'nvm needs curl or wget to proceed.'
3340-
return 1
3341-
fi
3342-
33433403
if [ $# -lt 1 ]; then
33443404
version_not_provided=1
33453405
fi
33463406

33473407
local nobinary
33483408
local nosource
33493409
local noprogress
3410+
local NVM_OFFLINE
33503411
nobinary=0
33513412
noprogress=0
33523413
nosource=0
3414+
NVM_OFFLINE=0
33533415
local LTS
33543416
local ALIAS
33553417
local NVM_UPGRADE_NPM
@@ -3392,6 +3454,10 @@ nvm() {
33923454
noprogress=1
33933455
shift
33943456
;;
3457+
--offline)
3458+
NVM_OFFLINE=1
3459+
shift
3460+
;;
33953461
--lts)
33963462
LTS='*'
33973463
shift
@@ -3468,6 +3534,11 @@ nvm() {
34683534
esac
34693535
done
34703536

3537+
if [ "${NVM_OFFLINE}" != 1 ] && ! nvm_has "curl" && ! nvm_has "wget"; then
3538+
nvm_err 'nvm needs curl or wget to proceed.'
3539+
return 1
3540+
fi
3541+
34713542
local provided_version
34723543
provided_version="${1-}"
34733544

@@ -3505,8 +3576,27 @@ nvm() {
35053576
esac
35063577

35073578
local EXIT_CODE
3508-
VERSION="$(NVM_VERSION_ONLY=true NVM_LTS="${LTS-}" nvm_remote_version "${provided_version}")"
3509-
EXIT_CODE="$?"
3579+
3580+
if [ "${NVM_OFFLINE}" = 1 ]; then
3581+
local OFFLINE_PATTERN
3582+
OFFLINE_PATTERN="${provided_version}"
3583+
if [ -n "${LTS-}" ]; then
3584+
if [ "${LTS}" = '*' ]; then
3585+
OFFLINE_PATTERN="$(nvm_resolve_alias 'lts/*' 2>/dev/null || nvm_echo)"
3586+
else
3587+
OFFLINE_PATTERN="$(nvm_resolve_alias "lts/${LTS}" 2>/dev/null || nvm_echo)"
3588+
fi
3589+
if [ -z "${OFFLINE_PATTERN}" ]; then
3590+
nvm_err "LTS alias '${LTS}' not found locally. Run \`nvm ls-remote --lts\` first to populate LTS aliases."
3591+
return 3
3592+
fi
3593+
fi
3594+
VERSION="$(nvm_offline_version "${OFFLINE_PATTERN}")"
3595+
EXIT_CODE="$?"
3596+
else
3597+
VERSION="$(NVM_VERSION_ONLY=true NVM_LTS="${LTS-}" nvm_remote_version "${provided_version}")"
3598+
EXIT_CODE="$?"
3599+
fi
35103600

35113601
if [ "${VERSION}" = 'N/A' ] || [ $EXIT_CODE -ne 0 ]; then
35123602
local LTS_MSG
@@ -3522,9 +3612,17 @@ nvm() {
35223612
return 3
35233613
fi
35243614
else
3525-
REMOTE_CMD='nvm ls-remote'
3615+
if [ "${NVM_OFFLINE}" = 1 ]; then
3616+
REMOTE_CMD='nvm ls'
3617+
else
3618+
REMOTE_CMD='nvm ls-remote'
3619+
fi
3620+
fi
3621+
if [ "${NVM_OFFLINE}" = 1 ]; then
3622+
nvm_err "Version '${provided_version}' ${LTS_MSG-}not found locally or in cache - try \`${REMOTE_CMD}\` to browse available versions."
3623+
else
3624+
nvm_err "Version '${provided_version}' ${LTS_MSG-}not found - try \`${REMOTE_CMD}\` to browse available versions."
35263625
fi
3527-
nvm_err "Version '${provided_version}' ${LTS_MSG-}not found - try \`${REMOTE_CMD}\` to browse available versions."
35283626
return 3
35293627
fi
35303628

@@ -3664,7 +3762,7 @@ nvm() {
36643762

36653763
# skip binary install if "nobinary" option specified.
36663764
if [ $nobinary -ne 1 ] && nvm_binary_available "${VERSION}"; then
3667-
NVM_NO_PROGRESS="${NVM_NO_PROGRESS:-${noprogress}}" nvm_install_binary "${FLAVOR}" std "${VERSION}" "${nosource}"
3765+
NVM_NO_PROGRESS="${NVM_NO_PROGRESS:-${noprogress}}" NVM_OFFLINE="${NVM_OFFLINE}" nvm_install_binary "${FLAVOR}" std "${VERSION}" "${nosource}"
36683766
EXIT_CODE=$?
36693767
else
36703768
EXIT_CODE=-1
@@ -3683,7 +3781,7 @@ nvm() {
36833781
nvm_err 'Installing from source on non-WSL Windows is not supported'
36843782
EXIT_CODE=87
36853783
else
3686-
NVM_NO_PROGRESS="${NVM_NO_PROGRESS:-${noprogress}}" nvm_install_source "${FLAVOR}" std "${VERSION}" "${NVM_MAKE_JOBS}" "${ADDITIONAL_PARAMETERS}"
3784+
NVM_NO_PROGRESS="${NVM_NO_PROGRESS:-${noprogress}}" NVM_OFFLINE="${NVM_OFFLINE}" nvm_install_source "${FLAVOR}" std "${VERSION}" "${NVM_MAKE_JOBS}" "${ADDITIONAL_PARAMETERS}"
36873785
EXIT_CODE=$?
36883786
fi
36893787
fi
@@ -4510,7 +4608,7 @@ nvm() {
45104608
nvm_binary_available nvm_change_path nvm_strip_path \
45114609
nvm_num_version_groups nvm_format_version nvm_ensure_version_prefix \
45124610
nvm_normalize_version nvm_is_valid_version nvm_normalize_lts \
4513-
nvm_ensure_version_installed nvm_cache_dir \
4611+
nvm_ensure_version_installed nvm_cache_dir nvm_ls_cached nvm_offline_version \
45144612
nvm_version_path nvm_alias_path nvm_version_dir \
45154613
nvm_find_nvmrc nvm_find_up nvm_find_project_dir nvm_tree_contains_path \
45164614
nvm_version_greater nvm_version_greater_than_or_equal_to \
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/sh
2+
3+
die () { echo "$@" ; exit 1; }
4+
5+
\. ../../../nvm.sh
6+
7+
\. ../../common.sh
8+
9+
# Mock nvm_download to ensure no network access
10+
nvm_download() {
11+
die "nvm_download should not be called in offline mode"
12+
}
13+
14+
# --offline with an already-installed version should succeed
15+
INSTALLED_VERSION="$(nvm ls | command tail -1 | command awk '{print $1}' | command sed 's/\x1b\[[0-9;]*m//g')"
16+
if [ -n "${INSTALLED_VERSION}" ] && [ "_${INSTALLED_VERSION}" != '_N/A' ] && [ "_${INSTALLED_VERSION}" != '_system' ]; then
17+
try nvm install --offline "${INSTALLED_VERSION}"
18+
[ "_$CAPTURED_EXIT_CODE" = "_0" ] \
19+
|| die "nvm install --offline with installed version '${INSTALLED_VERSION}' should succeed, got exit code $CAPTURED_EXIT_CODE"
20+
fi
21+
22+
# --offline with a nonexistent version should fail
23+
try_err nvm install --offline 999.999.999
24+
[ "_$CAPTURED_EXIT_CODE" != "_0" ] \
25+
|| die "nvm install --offline with nonexistent version should fail"
26+
27+
EXPECTED_ERR="not found locally or in cache"
28+
nvm_echo "$CAPTURED_STDERR" | nvm_grep -q "${EXPECTED_ERR}" \
29+
|| die "nvm install --offline error should mention 'not found locally or in cache'; got '$CAPTURED_STDERR'"
30+
31+
# --offline should not require curl or wget
32+
nvm_has() { return 1; }
33+
try_err nvm install --offline 999.999.999
34+
# Should fail with "not found" not "nvm needs curl or wget"
35+
nvm_echo "$CAPTURED_STDERR" | nvm_grep -q "curl or wget" \
36+
&& die "nvm install --offline should not require curl or wget"
37+
alias nvm_has='\nvm_has'
38+
unset -f nvm_has
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/sh
2+
3+
die () { echo "$@" ; cleanup ; exit 1; }
4+
5+
\. ../../../nvm.sh
6+
7+
\. ../../common.sh
8+
9+
TEST_DIR="$(pwd)/nvm_offline_version_tmp"
10+
11+
cleanup() {
12+
rm -rf "${TEST_DIR}"
13+
}
14+
15+
[ ! -e "${TEST_DIR}" ] && mkdir -p "${TEST_DIR}"
16+
17+
# nvm_offline_version should find installed versions
18+
INSTALLED_VERSION="$(nvm_version node)"
19+
if [ "_${INSTALLED_VERSION}" != '_N/A' ] && [ "_${INSTALLED_VERSION}" != '_system' ]; then
20+
try nvm_offline_version "${INSTALLED_VERSION}"
21+
[ "_$CAPTURED_STDOUT" = "_${INSTALLED_VERSION}" ] \
22+
|| die "nvm_offline_version '${INSTALLED_VERSION}' should return '${INSTALLED_VERSION}'; got '$CAPTURED_STDOUT'"
23+
[ "_$CAPTURED_EXIT_CODE" = "_0" ] \
24+
|| die "nvm_offline_version '${INSTALLED_VERSION}' should exit 0; got '$CAPTURED_EXIT_CODE'"
25+
fi
26+
27+
# nvm_offline_version with nonexistent version should return N/A
28+
try nvm_offline_version "999.999.999"
29+
[ "_$CAPTURED_STDOUT" = "_N/A" ] \
30+
|| die "nvm_offline_version '999.999.999' should return 'N/A'; got '$CAPTURED_STDOUT'"
31+
[ "_$CAPTURED_EXIT_CODE" = "_3" ] \
32+
|| die "nvm_offline_version '999.999.999' should exit 3; got '$CAPTURED_EXIT_CODE'"
33+
34+
# nvm_ls_cached with nonexistent pattern should return nothing
35+
try nvm_ls_cached "999.999"
36+
[ -z "$CAPTURED_STDOUT" ] \
37+
|| die "nvm_ls_cached '999.999' should return empty; got '$CAPTURED_STDOUT'"
38+
39+
cleanup

0 commit comments

Comments
 (0)