← Blog
Node.js14 min czytania

nodenv — przełączanie wersji Node.js osobno dla każdego projektu

nodenv przełącza wersję Node.js na podstawie pliku w katalogu projektu, przez podstawki w PATH. Wyjaśniamy podział na dwa repozytoria, trzy poziomy wyboru wersji i pułapkę z pakietami globalnymi, na której potyka się każdy.

Problem pojawia się przy drugim projekcie. Pierwszy chodzi na Node 20, bo tak było, gdy powstawał. Drugi wymaga 24, bo korzysta z czegoś nowego. Trzeci to narzędzie, które zainstalowałeś globalnie i które odmawia startu powyżej pewnej wersji — w tej serii opisywaliśmy niedawno inshellisense, który w polu engines deklaruje Node w przedziale od 18 do poniżej 23. Trzy różne wymagania, jeden komputer.

Ręczne przeinstalowywanie Node'a przy każdym przejściu między katalogami jest oczywiście nie do przyjęcia. nodenv rozwiązuje to w sposób, który po zrozumieniu okazuje się bardzo prosty: wersję Node.js określa plik leżący w katalogu projektu, a przełączenie następuje samo, w momencie wejścia do tego katalogu.

Stan projektu i dlaczego pozorna cisza nie jest problemem

Repozytorium: nodenv/nodenv. 2414 gwiazdek, 164 forki, licencja MIT z prawami Willa McKenziego i Sama Stephensona, datowana na 2013 — projekt ma trzynaście lat. Otwartych zgłoszeń jest dziesięć.

Na pierwszy rzut oka wygląda to na projekt uśpiony. Ostatnie wydanie, v1.6.2, ukazało się 30 lipca 2025, a poprzednie: v1.5.0 w maju 2024, v1.4.1 w lutym 2023 i v1.4.0 w lipcu 2020. Świeże commity na gałęzi main — z 7 września 2026 — to niemal wyłącznie podbicia zależności od bota, scalane przez opiekuna.

Byłby to niepokojący obraz, gdyby nie architektura tego projektu, która wyjaśnia wszystko. nodenv jest rozdzielony na dwa repozytoria, a to, co wymaga częstych zmian, leży w drugim z nich.

Polecenie nodenv install nie należy do nodenv — dostarcza je wtyczka node-build. I tam sytuacja wygląda zupełnie inaczej: commity z 9 września 2026, czyli z dnia poprzedzającego powstanie tego wpisu, oraz dwa wydania tego samego dnia, v5.4.53 i v5.4.52. W historii widać bota o nazwie nodenv-bot, który dorzuca definicje nowych wersji Node'a — wśród świeżych wpisów są 26.8.2 i 24.21.0, a obok nich commity opiekuna opisane jako „zeskrobane najnowsze definicje".

To jest projektowa lekcja warta zapamiętania niezależnie od samego narzędzia: część szybko zmienna została oddzielona od części stabilnej. Rdzeń, który tylko przełącza wersje, nie ma powodu zmieniać się co tydzień i nie zmienia się. Katalog dostępnych wersji Node'a zmienia się co kilka dni i jest aktualizowany automatycznie. Ocena zdrowia takiego projektu na podstawie samego rdzenia byłaby więc błędem — i przy okazji jest to ostrzeżenie dla każdego, kto ocenia zależności po dacie ostatniego commita w głównym repozytorium.

Jak to działa

Mechanizm opiera się na podstawkach (shims). Podczas instalacji nodenv dopisuje się do zmiennej PATH, a konkretnie przedstawia w niej katalog ~/.nodenv/shims. Dokumentacja mówi o tym wprost i warto to zdanie zapamiętać: przedstawienie katalogu podstawek w PATH jest zasadniczo jedynym wymogiem, by nodenv działał.

W efekcie każde wywołanie node, npm, npx czy dowolnego innego programu z ekosystemu Node'a najpierw uruchamia nodenv. Ten skanuje bieżący katalog projektu w poszukiwaniu pliku o nazwie .node-version, a znalezioną tam wersję odnajduje wśród zainstalowanych w ~/.nodenv/versions/ i przekazuje jej wywołanie.

cd mojprojekt
nodenv local 24.1.0

To polecenie zapisuje wybraną wersję do pliku .node-version w bieżącym katalogu. Inny projekt w innym katalogu może używać całkiem innej wersji — przejście między nimi nie wymaga żadnej dodatkowej czynności.

Wersje można też dostarczyć samodzielnie. Wystarczy umieścić skompilowanego Node'a jako podkatalog w ~/.nodenv/versions, przy czym wpis w tym katalogu może być również symbolicznym odnośnikiem do instalacji leżącej gdzie indziej w systemie. To przydatna furtka, jeśli macie własne, firmowe kompilacje albo Node'a dostarczanego przez inne narzędzie.

Warto przy okazji odnotować, że przykład w dokumentacji pokazuje w liście wersji wpisy w rodzaju graal+ce-19.2.1 i iojs-3.3.1 — mechanizm nie jest więc przywiązany do jednej implementacji.

Trzy poziomy wyboru wersji

Wersja jest wybierana według jasnej hierarchii, od najwyższego priorytetu:

  • Poziom powłoki — nodenv shell 20.7.0, działający przez zmienną NODENV_VERSION. Przebija wszystko pozostałe i obowiązuje tylko w tej sesji. Wymaga włączonej integracji z powłoką; bez niej można po prostu samodzielnie wyeksportować NODENV_VERSION.
  • Poziom projektu — plik .node-version, ustawiany przez nodenv local. Przebija ustawienie globalne.
  • Poziom globalny — plik ~/.nodenv/version, ustawiany przez nodenv global. Obowiązuje wszędzie, gdzie nie ma nic bardziej szczegółowego.

Każdy z tych poziomów da się wyczyścić przełącznikiem --unset, a wywołanie polecenia bez numeru wersji wypisuje aktualnie ustawioną wartość. Jest też specjalna nazwa wersji system, która każe nodenv używać Node'a systemowego, wyszukanego w PATH — przydatna, gdy chcesz tymczasowo zejść z drogi.

Instalacja

Na macOS i Linuksie autorzy zalecają Homebrew, a poza tym dostępny jest zwykły klon repozytorium do ~/.nodenv. Ciekawostką jest trzeci kanał: nodenv jest publikowany również na npm jako @nodenv/nodenv, co przy menedżerze wersji Node'a jest zabawnie okrężne, ale bywa wygodne.

Dokumentacja jest przy tym uczciwa co do pakietów systemowych i wyróżnia to ostrzeżeniami: nodenv nie jest obecnie dostępny w repozytoriach Debiana, Ubuntu ani Fedory, i w obu przypadkach zalecana jest instalacja z gita, z dopiskiem „rozważ kontrybucję". Arch ma paczkę w AUR.

Jest jeszcze szczegół, który potrafi zirytować: uzupełnianie polecenia dla Zsh nie ładuje się samo. Skrypty dla Basha i fisha wczytuje mechanizm nodenv init, ale przy Zsh trzeba samodzielnie dopisać katalog do zmiennej FPATH i wywołać compinit. To nie awaria, tylko krok, o którym łatwo nie wiedzieć.

Co robi nodenv init

Warto to rozumieć, bo dotyka twojego pliku startowego powłoki. Polecenie ma dwa tryby: nodenv init — przewidziany dla ludzi — edytuje pliki inicjalizacyjne powłoki na dysku, dopisując do nich obsługę nodenv. Dokumentacja zaznacza, że przed wersją 1.6.0 ten tryb tylko wypisywał instrukcje, a niczego nie zmieniał, więc jeśli pamiętasz nodenv z dawniejszych czasów, to zachowanie się zmieniło. Drugi tryb, nodenv init -, jest przewidziany dla maszyn i wypisuje skrypt do wykonania przez powłokę.

Dopisywana linia wygląda tak i można ją równie dobrze wstawić ręcznie:

eval "$(nodenv init - --no-rehash bash)"

Wykonywany skrypt robi cztery rzeczy: dodaje program nodenv do PATH, jeśli trzeba; przedstawia w PATH katalog podstawek (to jedyny właściwy wymóg); instaluje uzupełnianie polecenia dla Basha; odtwarza podstawki. Ten ostatni krok potrafi opóźniać start powłoki, dlatego istnieje flaga --no-rehash — i, co warto zauważyć, jest ona domyślnie obecna w generowanej linii. Jest też piąty, opcjonalny element: „dyspozytor sh", który pozwala nodenv i wtyczkom zmieniać zmienne w twojej bieżącej powłoce, i to on umożliwia działanie polecenia nodenv shell.

Pakiety globalne, czyli pułapka, na którą trafia każdy

To najważniejsza praktyczna konsekwencja tego modelu i najczęstsze źródło zdziwienia.

Pakiety instalowane globalnie są przypisane do aktualnie aktywnej wersji Node.js. Nie do systemu, nie do użytkownika — do wersji. Można to sprawdzić poleceniem npm prefix -g, które zwróci ścieżkę w rodzaju ~/.nodenv/versions/<wersja>. Praktycznie znaczy to, że narzędzie zainstalowane globalnie pod Node 24 zniknie po przełączeniu na Node 20 — i to jest zachowanie zamierzone, bo dokładnie o taką izolację chodzi. Jeśli macie zestaw pakietów, które chcecie mieć w każdej wersji, jest do tego wtyczka nodenv-default-packages.

Druga strona tej samej sprawy dotyczy podstawek. Po zainstalowaniu albo odinstalowaniu pakietu globalnego może być konieczne wywołanie nodenv rehash, które tworzy podstawki dla wszystkich programów znanych nodenv, czyli dla zawartości ~/.nodenv/versions/*/bin/*. Bez tego świeżo zainstalowane narzędzie zwyczajnie nie zostanie znalezione w PATH — i to jest ten moment, w którym ludzie myślą, że instalacja się nie udała. Istnieje wtyczka nodenv-package-rehash, która robi to automatycznie i warto ją mieć od początku.

Jest jeszcze porada, którą dokumentacja formułuje wyróżnieniem, i jest ona trafna: nie używaj sudo do instalowania pakietów. Wersje Node'a leżą w twoim katalogu domowym i są zapisywalne przez twojego użytkownika, więc sudo nie jest potrzebne. A jeśli dostajesz błąd o braku uprawnień do zapisu, to najprawdopodobniej — i to jest właściwa diagnoza — domyślną wersją globalną jest nadal Node systemowy. Naprawia się to poleceniem nodenv global <wersja>, nie podnoszeniem uprawnień.

Diagnostyka, czyli najlepsza część

Menedżer wersji jest narzędziem, które zawodzi w sposób mylący — coś działa nie tak, a ty nie wiesz, która wersja jest aktywna i skąd się wzięła. nodenv odpowiada na to trzema poleceniami, które warto znać na pamięć.

nodenv version wypisuje aktywną wersję razem z informacją, skąd została ustawiona:

$ nodenv version
24.4.1 (set by /Users/jasonkarns/code/testdouble/.node-version)

To jedno zdanie rozwiązuje większość zagadek. Podobnie nodenv versions wypisuje wszystkie znane wersje z gwiazdką przy aktywnej i tą samą adnotacją o pochodzeniu.

nodenv which npm podaje pełną ścieżkę programu, który faktycznie zostanie uruchomiony — czyli odpowiada na pytanie „który npm to właściwie jest". A nodenv whence yarn odwraca pytanie i wypisuje wszystkie wersje Node'a, w których dany program jest zainstalowany. Przy szukaniu, gdzie się podziało narzędzie, jest to najkrótsza droga.

Do tego dochodzi zmienna NODENV_DEBUG (albo przełącznik --debug) wypisująca informacje diagnostyczne, oraz nodenv prefix <wersja>, która wskazuje katalog danej wersji — przydatna, bo odinstalowanie wersji to po prostu usunięcie tego katalogu.

Pozostałe zmienne środowiskowe warte zapamiętania: NODENV_ROOT (domyślnie ~/.nodenv) przenosi cały magazyn wersji i podstawek w inne miejsce, a NODENV_DIR (domyślnie bieżący katalog) wskazuje, od którego katalogu zaczyna się szukanie plików .node-version.

Wtyczki, które rozwiązują opisane wyżej problemy

Rozszerzalność nie jest tu deklaracją — organizacja projektu utrzymuje kilkanaście wtyczek i, co istotne, wszystkie były ruszane w ciągu ostatnich dwóch miesięcy. Kilka z nich odpowiada wprost na trudności, o których pisałem powyżej.

  • nodenv-package-json-engine — czyta wymaganie wersji z pola engines w package.json. To domyka lukę, o której mówiłem: zamiast pilnować spójności dwóch źródeł prawdy, pozwalasz nodenv czytać to, które już masz w projekcie.
  • nodenv-nvmrc — obsługuje plik .nvmrc. Dzięki temu można przejść na nodenv w zespole, w którym część osób używa nvm, bez zmuszania nikogo do zmiany pliku w repozytorium.
  • nodenv-npm-migrate — przenosi pakiety globalne między wersjami Node'a. To wprost odpowiedź na zniknięcie narzędzi po przełączeniu wersji.
  • nodenv-package-rehash — odtwarza podstawki automatycznie po instalacji pakietu, czyli usuwa konieczność pamiętania o nodenv rehash.
  • nodenv-default-packages — zestaw pakietów instalowanych globalnie w każdej wersji.
  • nodenv-aliases — nazwy zastępcze dla wersji, oraz nodenv-update aktualizujący nodenv wraz z wtyczkami jednym poleceniem.
  • nodenv-vars — zmienne środowiskowe ustawiane per wersja, oraz nodenv-each uruchamiający polecenie we wszystkich zainstalowanych wersjach (przydatne przy sprawdzaniu zgodności biblioteki).

Osobno warto wymienić dwie rzeczy poza samą powłoką. Najpopularniejszą wtyczką w tej organizacji jest jetbrains-npm, czyli integracja ze środowiskami JetBrains — co przy tej serii, pisanej w projekcie na PhpStormie, jest informacją praktyczną. Są też gotowe akcje dla GitHub Actions (actions-setup-nodenv i actions-node-version), dzięki którym potok CI używa tej samej wersji, którą deklaruje repozytorium.

Uczciwa uwaga do tego ekosystemu: kilka wtyczek nie ma pliku licencji — dotyczy to między innymi nodenv-package-json-engine, nodenv-nvmrc oraz obu akcji dla GitHub Actions. Sam nodenv i większość wtyczek są na MIT (instalator nawet na CC0), ale przy audycie zależności te pozycje trzeba odnotować osobno, bo brak licencji formalnie oznacza zastrzeżenie wszystkich praw.

Na koniec drobiazg dla osób automatyzujących: katalog wykonywalny nodenv zawiera dwadzieścia pięć poleceń, w tym takie, których nie ma w skróconej dokumentacji — version-origin podaje samo źródło ustawienia wersji, a version-file-read i version-file-write operują na pliku wersji wprost. To wygodniejsze do skryptowania niż parsowanie komunikatu „set by" z wyjścia dla ludzi.

Zastosowanie firmowe

Największa wartość tego narzędzia w zespole nie polega na wygodzie pojedynczej osoby, a na tym, że plik .node-version trafia do repozytorium. Wersja Node.js przestaje być wiedzą plemienną przekazywaną na wdrożeniu i staje się częścią kodu — sprawdzalną, wersjonowaną i widoczną w diffie, gdy ktoś ją podnosi.

Warto to skojarzyć z polem engines w package.json, które deklaruje wymagania, ale ich nie zapewnia. engines mówi, czego projekt potrzebuje; .node-version mówi, co faktycznie zostanie uruchomione. Trzymanie obu spójnych jest dobrą praktyką, a rozjechanie się ich to typowa przyczyna sytuacji „u mnie działa".

Ten sam plik jest przy okazji rozpoznawany przez inne narzędzia i przez część usług ciągłej integracji, więc jego obecność w repozytorium bywa jedynym potrzebnym ustawieniem, by potok budował się na tej samej wersji, na której pracują ludzie.

Trzeba przy tym powiedzieć uczciwie, że nodenv nie jest jedynym rozwiązaniem tego problemu i sami autorzy tego nie ukrywają — README odsyła do porównania menedżerów wersji, ze zdaniem, że prostota nodenv ma swoje zalety, ale też pewne wady. To rzadko spotykana szczerość: projekt sam wskazuje, gdzie szukać alternatyw.

Pułapki w jednym miejscu

  • Pakiety globalne są przypisane do wersji Node'a — po przełączeniu wersji znikają. To zachowanie zamierzone; do pakietów wspólnych jest wtyczka nodenv-default-packages.
  • Po instalacji pakietu globalnego może być potrzebne nodenv rehash, inaczej nowe narzędzie nie zostanie znalezione. Rozwiązuje to wtyczka nodenv-package-rehash.
  • Nie używaj sudo. Błąd o braku uprawnień do zapisu zwykle znaczy, że globalnie ustawiony jest Node systemowy.
  • nodenv install nie należy do nodenv — dostarcza je wtyczka node-build, którą przy instalacji z gita trzeba dołożyć osobno.
  • Uzupełnianie polecenia dla Zsh wymaga ręcznego dopisania FPATH i wywołania compinit.
  • nodenv init od wersji 1.6.0 edytuje pliki startowe powłoki na dysku, a nie tylko wypisuje instrukcje.
  • Odtwarzanie podstawek przy starcie powłoki potrafi ją opóźniać — stąd flaga --no-rehash, obecna domyślnie w generowanej linii.
  • Brak paczek w repozytoriach Debiana, Ubuntu i Fedory. Zostaje Homebrew, AUR albo git.
  • Część wtyczek nie ma pliku licencji — przy audycie zależności trzeba je odnotować osobno.
  • Systemy uniksowe. Narzędzie jest napisane w powłoce i przeznaczone dla systemów uniksopodobnych.
  • Nie oceniaj tego projektu po dacie commitów w głównym repozytorium — częste zmiany dzieją się w node-build.

Podsumowanie

nodenv jest narzędziem, które po zrozumieniu przestaje być widoczne — i to jest największy komplement, jaki można zrobić menedżerowi wersji. Wchodzisz do katalogu, dostajesz właściwą wersję, wychodzisz i dostajesz inną. Trzynaście lat historii i dziesięć otwartych zgłoszeń mówią, że ten model się sprawdził.

  • Wersja jako część repozytorium — plik .node-version zamiast ustaleń przekazywanych ustnie.
  • Trzy poziomy wyboru — powłoka, projekt i ustawienie globalne, z jasnym pierwszeństwem i specjalną wartością system.
  • Diagnostyka, która odpowiada na właściwe pytania — version z podaniem źródła ustawienia, which i whence.
  • Rozdzielenie części stabilnej od szybko zmiennej — rdzeń jest spokojny, a definicje wersji dorzuca bot w node-build w ciągu dni od wydania Node'a.
  • Rozszerzalność wtyczkami w Bashu, obejmująca niemal każdy element mechanizmu.
  • Prosta furtka na własne kompilacje — wpis w katalogu wersji może być odnośnikiem symbolicznym do instalacji leżącej gdziekolwiek.

Projekt jest wydany na licencji MIT z prawami Willa McKenziego i Sama Stephensona — a to drugie nazwisko warto zauważyć, bo jest autorem rbenv, narzędzia dla Rubiego, którego konstrukcję nodenv przenosi na Node.js. Licencja jest permisywna: możesz używać komercyjnie, modyfikować i dostarczać klientom bez obowiązku otwierania własnego kodu. Przy narzędziu napisanym w całości w powłoce, którego cały mechanizm sprowadza się do jednego katalogu przedstawionego w PATH, ma to jeszcze jedną wartość — jeśli kiedykolwiek zajdzie potrzeba, przeczytanie i naprawienie tego samodzielnie jest w zasięgu jednej osoby na jedno popołudnie.