#!/usr/bin/env bash # Prefetch official Node, Corepack pnpm, and lockfile packages into in-repo caches. # Run on the same OS/CPU as the later install, while nodejs.org and the npm registry # are reachable. Node/pnpm cannot use socks5h:// proxies; pass an HTTP proxy via # DSH_NPM_HTTP_PROXY (example: http://127.0.0.1:3067). That overlay is applied only # around Corepack/pnpm and is restored before the script returns, so a caller SOCKS # proxy stays in the current shell. curl keeps the inherited proxy (SOCKS included). # Optional DSH_NODE_VERSION (no leading v) pins the Node distro; otherwise the # running Node is used, or the newest v24.x from nodejs.org/dist/index.json. # # Usage (repo root): bash scripts/offline/download-deps.sh set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" cd "$ROOT" process_var() { local name="$1" if [[ -n "${!name+x}" ]]; then printf '%s' "${!name}" fi } inherited_proxy() { local name value for name in HTTPS_PROXY HTTP_PROXY ALL_PROXY https_proxy http_proxy all_proxy; do value="$(process_var "$name")" if [[ -n "$value" ]]; then printf '%s' "$value" return fi done } resolve_node_http_proxy() { local override socks name value override="$(process_var DSH_NPM_HTTP_PROXY)" if [[ -n "$override" ]]; then if [[ "$override" == socks5* ]]; then echo "DSH_NPM_HTTP_PROXY=$override. Node and pnpm cannot fetch through SOCKS. Use an HTTP proxy (for example http://127.0.0.1:3067)." >&2 exit 1 fi printf '%s' "$override" return fi socks= for name in HTTPS_PROXY HTTP_PROXY ALL_PROXY https_proxy http_proxy all_proxy; do value="$(process_var "$name")" if [[ -z "$value" ]]; then continue fi if [[ "$value" == socks5* ]]; then socks="$name=$value" continue fi printf '%s' "$value" return done if [[ -n "$socks" ]]; then echo "$socks. Node and pnpm cannot fetch through SOCKS. Set DSH_NPM_HTTP_PROXY to an HTTP proxy (for example http://127.0.0.1:3067)." >&2 exit 1 fi } run_with_node_http_proxy() { local http status http="$(resolve_node_http_proxy)" if [[ -z "$http" ]]; then "$@" return fi local HTTPS_PROXY_BAK="${HTTPS_PROXY-}" HTTP_PROXY_BAK="${HTTP_PROXY-}" ALL_PROXY_BAK="${ALL_PROXY-}" local https_proxy_bak="${https_proxy-}" http_proxy_bak="${http_proxy-}" all_proxy_bak="${all_proxy-}" local had_HTTPS_PROXY=0 had_HTTP_PROXY=0 had_ALL_PROXY=0 local had_https_proxy=0 had_http_proxy=0 had_all_proxy=0 if [[ -n "${HTTPS_PROXY+x}" ]]; then had_HTTPS_PROXY=1; fi if [[ -n "${HTTP_PROXY+x}" ]]; then had_HTTP_PROXY=1; fi if [[ -n "${ALL_PROXY+x}" ]]; then had_ALL_PROXY=1; fi if [[ -n "${https_proxy+x}" ]]; then had_https_proxy=1; fi if [[ -n "${http_proxy+x}" ]]; then had_http_proxy=1; fi if [[ -n "${all_proxy+x}" ]]; then had_all_proxy=1; fi export HTTPS_PROXY="$http" HTTP_PROXY="$http" ALL_PROXY="$http" export https_proxy="$http" http_proxy="$http" all_proxy="$http" set +e "$@" status=$? set -e if (( had_HTTPS_PROXY )); then export HTTPS_PROXY="$HTTPS_PROXY_BAK"; else unset HTTPS_PROXY; fi if (( had_HTTP_PROXY )); then export HTTP_PROXY="$HTTP_PROXY_BAK"; else unset HTTP_PROXY; fi if (( had_ALL_PROXY )); then export ALL_PROXY="$ALL_PROXY_BAK"; else unset ALL_PROXY; fi if (( had_https_proxy )); then export https_proxy="$https_proxy_bak"; else unset https_proxy; fi if (( had_http_proxy )); then export http_proxy="$http_proxy_bak"; else unset http_proxy; fi if (( had_all_proxy )); then export all_proxy="$all_proxy_bak"; else unset all_proxy; fi return "$status" } curl_proxy_args=() curl_proxy="$(inherited_proxy)" if [[ -z "$curl_proxy" ]]; then curl_proxy="$(process_var DSH_NPM_HTTP_PROXY)" fi if [[ -n "$curl_proxy" ]]; then curl_proxy_args=(--proxy "$curl_proxy") fi save_url() { local url="$1" local dest="$2" mkdir -p "$(dirname "$dest")" curl -fsSL --retry 3 "${curl_proxy_args[@]}" -o "$dest" "$url" } node_platform() { local sys arch sys="$(uname -s)" arch="$(uname -m)" case "$sys:$arch" in Linux:x86_64) echo linux-x64 ;; Linux:aarch64) echo linux-arm64 ;; Darwin:x86_64) echo darwin-x64 ;; Darwin:arm64) echo darwin-arm64 ;; MINGW*|MSYS*|CYGWIN*) echo "Use scripts/offline/download-deps.ps1 on Windows." >&2 exit 1 ;; *) echo "Unsupported OS/CPU: $sys $arch" >&2 exit 1 ;; esac } archive_name() { local version="$1" local platform="$2" case "$platform" in linux-*) echo "node-v${version}-${platform}.tar.xz" ;; darwin-*) echo "node-v${version}-${platform}.tar.gz" ;; *) echo "node-v${version}-${platform}.zip" ;; esac } file_tag() { case "$1" in linux-x64) echo linux-x64 ;; linux-arm64) echo linux-arm64 ;; darwin-x64) echo osx-x64-tar ;; darwin-arm64) echo osx-arm64-tar ;; win-x64) echo win-x64-zip ;; win-arm64) echo win-arm64-zip ;; esac } engines_ok() { local version="$1" local major minor major="${version%%.*}" minor="${version#*.}" minor="${minor%%.*}" if (( major == 22 && minor >= 19 )); then return 0; fi if (( major >= 24 )); then return 0; fi return 1 } resolve_node_version() { local platform="$1" local dist_base="$2" if [[ -n "${DSH_NODE_VERSION:-}" ]]; then echo "${DSH_NODE_VERSION#v}" return fi if command -v node >/dev/null 2>&1; then local running running="$(node --version)" running="${running#v}" if ! engines_ok "$running"; then echo "Running Node v$running is outside engines.node (^22.19.0 || >=24.0.0). Set DSH_NODE_VERSION." >&2 exit 1 fi echo "$running" return fi local index_path tag version index_path="$(mktemp)" save_url "$dist_base/index.json" "$index_path" tag="$(file_tag "$platform")" if command -v python3 >/dev/null 2>&1; then version="$(python3 -c 'import json,sys releases=json.load(open(sys.argv[1])) tag=sys.argv[2] for r in releases: if r["version"].startswith("v24.") and tag in r["files"]: print(r["version"][1:]) break ' "$index_path" "$tag")" else echo "python3 or node is required to pick a Node v24.x from index.json, or set DSH_NODE_VERSION." >&2 rm -f "$index_path" exit 1 fi rm -f "$index_path" if [[ -z "$version" ]]; then echo "No Node v24.x archive published for $platform at $dist_base." >&2 exit 1 fi echo "$version" } assert_checksum() { local sums_path="$1" local archive_path="$2" local archive="$3" local expected actual expected="$(awk -v name="$archive" '$2 == name { print $1; exit }' "$sums_path")" if [[ -z "$expected" ]]; then echo "SHASUMS256.txt has no entry for $archive" >&2 exit 1 fi if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "$archive_path" | awk '{ print $1 }')" else actual="$(shasum -a 256 "$archive_path" | awk '{ print $1 }')" fi if [[ "$actual" != "$expected" ]]; then echo "SHA256 mismatch for $archive" >&2 exit 1 fi } extract_node() { local archive_path="$1" local runtime_dir="$2" mkdir -p "$runtime_dir" case "$archive_path" in *.tar.xz) tar -xJf "$archive_path" -C "$runtime_dir" ;; *.tar.gz) tar -xzf "$archive_path" -C "$runtime_dir" ;; *.zip) unzip -q "$archive_path" -d "$runtime_dir" ;; *) echo "Unknown Node archive: $archive_path" >&2 exit 1 ;; esac } PACKAGE_MANAGER="$(sed -n 's/.*"packageManager": "\(pnpm@[0-9.]*\)".*/\1/p' package.json | head -n 1)" case "$PACKAGE_MANAGER" in pnpm@*) ;; *) echo "package.json packageManager must be pnpm@, got ${PACKAGE_MANAGER:-}" >&2 exit 1 ;; esac DIST_BASE="${DSH_NODE_DIST_BASE:-https://nodejs.org/dist}" DIST_BASE="${DIST_BASE%/}" PLATFORM="$(node_platform)" NODE_VERSION="$(resolve_node_version "$PLATFORM" "$DIST_BASE")" ARCHIVE_NAME="$(archive_name "$NODE_VERSION" "$PLATFORM")" NODE_CACHE="$ROOT/.offline-cache/node" ARCHIVE_PATH="$NODE_CACHE/$ARCHIVE_NAME" SUMS_PATH="$NODE_CACHE/SHASUMS256-v${NODE_VERSION}.txt" RUNTIME_DIR="$NODE_CACHE/runtime" NODE_PREFIX="$RUNTIME_DIR/node-v${NODE_VERSION}-${PLATFORM}" echo "Prefetch Node v$NODE_VERSION ($PLATFORM)" save_url "$DIST_BASE/v$NODE_VERSION/SHASUMS256.txt" "$SUMS_PATH" save_url "$DIST_BASE/v$NODE_VERSION/$ARCHIVE_NAME" "$ARCHIVE_PATH" assert_checksum "$SUMS_PATH" "$ARCHIVE_PATH" "$ARCHIVE_NAME" if [[ ! -x "$NODE_PREFIX/bin/node" ]]; then rm -rf "$RUNTIME_DIR" extract_node "$ARCHIVE_PATH" "$RUNTIME_DIR" fi if [[ ! -x "$NODE_PREFIX/bin/node" ]]; then echo "Unpacked Node is missing: $NODE_PREFIX/bin/node" >&2 exit 1 fi printf '%s\n' "{\"version\":\"$NODE_VERSION\",\"archive\":\"$ARCHIVE_NAME\",\"platform\":\"$PLATFORM\"}" > "$NODE_CACHE/runtime.json" STORE_DIR="$ROOT/.offline-store" COREPACK_HOME="$ROOT/.offline-cache/corepack" mkdir -p "$STORE_DIR" "$COREPACK_HOME" export COREPACK_HOME export PATH="$NODE_PREFIX/bin:$PATH" echo "Node $("$NODE_PREFIX/bin/node" --version); prefetch $PACKAGE_MANAGER into $STORE_DIR" run_with_node_http_proxy corepack prepare "$PACKAGE_MANAGER" --activate run_with_node_http_proxy corepack pnpm fetch --frozen-lockfile --store-dir "$STORE_DIR" cat <