Repository: tj/n Branch: master Commit: f52d2172f12c Files: 41 Total size: 129.8 KB Directory structure: gitextract_ufb8xnx0/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── feature_request.md │ │ └── support.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .markdownlint.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── bin/ │ ├── dev/ │ │ └── release │ └── n ├── docs/ │ ├── changing-node-location.md │ └── proxy-server.md ├── package.json └── test/ ├── bin/ │ └── run-all-tests ├── docker-base.yml ├── docker-compose.yml ├── dockerfiles/ │ ├── Dockerfile-fedora-curl │ ├── Dockerfile-ubuntu-curl │ └── Dockerfile-ubuntu-wget ├── tests/ │ ├── install-contents.bats │ ├── install-options.bats │ ├── install-versions.bats │ ├── lookup.bats │ ├── ls.bats │ ├── lsr.bats │ ├── offline.bats │ ├── run-which.bats │ ├── shared-functions.bash │ ├── uninstall.bats │ ├── version-auto-priority.bats │ ├── version-resolve-auto-engine.bats │ ├── version-resolve-auto-file.bats │ ├── version-resolve-auto-nvmrc.bats │ └── version-resolve.bats └── tests.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true [*] end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 tab_width = 2 trim_trailing_whitespace = true # Tab indentation (no size specified) [{Makefile,makefile}] indent_style = tab [*.md] # indent 4 for code blocks indent_size = 4 # preserve trailing space for line breaks trim_trailing_whitespace = false ================================================ FILE: .github/FUNDING.yml ================================================ github: [tj, shadowspawn] tidelift: npm/n ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug Report about: Found a bug? --- # Bug Report ## Summary ## Steps to Reproduce ## Expected Behaviour ## Actual Behaviour ## Other Information ## Configuration Details ```bash $ n --version ? $ command -v node ? $ node -p process.platform ? ``` ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ -- name: Feature request about: Suggest an idea for this project --- # Feature Request ## Problem ## Proposed Solution ================================================ FILE: .github/ISSUE_TEMPLATE/support.md ================================================ --- name: Support about: Got a problem? --- # Problem ## Short Version ## Long Version ## Configuration Details ```bash $ n --version ? $ command -v node ? $ node -p process.platform ? ``` ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ # Pull Request ## Problem ## Solution ## ChangeLog ================================================ FILE: .gitignore ================================================ .DS_Store node_modules test/proxy~~.dump ================================================ FILE: .markdownlint.json ================================================ { "ul-indent": { "indent": 4 }, "line-length": false } ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] (date goes here) ## [10.2.0] (2025-05-21) ### Added - environment variable `N_ARCH` to set architecture for download, like using `--arch` ([#832]) ## [10.1.0] (2024-11-09) ### Added - `--cleanup` option to delete cached version after install for a one-shot install ([#818]) - `download` command to download Node.js version into cache ([#821]) - document using `--download` with run/exec/which in README - support trailing comments in `.nvmrc` file ([#820]) - mask password in download URL when displayed ([#815]) ### Changed - removed stale code ([#817]) ## [10.0.0] (2024-09-06) The major version bump is due to using `jq` in preference to `node` for reading `package.json`. ### Changed - if `jq` is available, use `jq` for reading `engines` from `package.json` instead of using `node` ([#810]) - avoid a network lookup if auto or engine find a fully specified numeric version. ([#813]) ## [9.2.3] (2024-04-21) ### Fixed - avoid problems with `curl` 8.7.1 and `--compressed` by removing option until fixed ## [9.2.2] (2024-04-21) (No changes.) ## [9.2.1] (2024-02-25) ### Fixed - `n doctor` works with custom `N_CACHE_PREFIX` ### Added - expand tests in `n doctor` for folder existence and permissions ## [9.2.0] (2023-10-15) ### Added - `--offline` for resolving target version against cached downloads instead of internet lookup ([#785]) ## [9.1.0] (2023-04-15) ### Added - check for possible problem with multiple `npm` locations when running `n doctor` ([#764]) ## [9.0.1] (2022-11-04) ### Fixed - `Makefile` compatible with more flavours of `make` ([#745]) - quote paths in `Makefile` in case `PREFIX` contains spaces ([#746]) ## [9.0.0] (2022-07-16) ### Changed - `--preserve` preserves `corepack` too ([#736]) ### Added - optional `N_PRESERVE_COREPACK` to change default behaviour for preserving `corepack` ([#736]) ## [8.2.0] (2022-04-18) ### Added - log before copying files during install ([#720]) ## [8.1.0] (2022-03-18) ### Added - optional `N_CACHE_PREFIX` for separate location for downloaded files than install location ([#717]) ## [8.0.2] (2022-01-09) ### Fixed - improved warning message when utility location hash may be holding old location to cover a wider range of shells including dash ([#707]) ## [8.0.1] (2021-12-04) ### Fixed - improve error handling for tar extraction errors ([#701]) - add tar flag for compatibility with tar builds which do not default to stdin ([#697]) ## [8.0.0] (2021-10-23) ### Changed - display error if version missing in version file for `n auto` and `n engine` (rather than fallback to current) ([#693]) ## [7.5.0] (2021-09-26) ### Added - support for Corepack (which was added to Node.js in v16.9.0) ## [7.4.1] (2021-09-11) ### Fixed - run commands from correct directory after `--download` causes a download ## [7.4.0] (2021-09-10) ### Added - support for `--download` option to `run` and `exec` to download the target version when needed ([#685]) ## [7.3.1] (2021-07-25) ### Changed - Improved README for new users missing expected folders in `/usr/local` ([#679]) ## [7.3.0] (2021-06-06) ### Added - ls-remote supports `engine` and `auto` labels ([#675]) - reduce `engine` and `auto` logging with `--quiet` ([#675]) - add WSL support to README ([#676]) - support for Emacs up and down keys (`ctrl-p` and `ctrl-n`) ([#669]) ### Changed - diagnostic logging during processing of engine and auto written to stderr rather than stdout ([#675]) ## [7.2.2] (2021-04-25) ### Fixed - arrow key navigation of version menu when terminal in application mode (e.g. PowerShell on Mac) ([#668]) ## [7.2.1] (2021-04-19) ### Added - install native arm64 Node.js on Macs with Apple silicon for Node.js 16 and higher ([#664]) ## [7.2.0] (2021-04-19) [YANKED] Released off wrong branch, essentially same as 7.1.0. ## [7.1.0] (2021-03-12) ### Added - support installs where /usr/local/share/man is a symlink (such as archlinux) - remove requirement for rsync for --preserve - avoid install pollution if user installs global packages when using n exec ## [7.0.2] (2021-02-27) ### Fixed - consistently log to STDOUT ([#654]) ## [7.0.1] (2021-01-30) ### Changed - update bats, and use bats-assert for better unit test failure messages ### Fixed - fail to display error in some cases for missing both `curl` and `wget` ([#649]) ## [7.0.0] (2020-12-20) ### Changed - `auto` label now scans for `package.json` only if it can not find a version control file ([#644]) ### Added - `engine` label to look for `engines.node` in `package.json` (as used by`auto`) ([#644]) ### Fixed - avoid colorized grep output via `GREP_OPTIONS` breaking version lookup ([#643]) ## [6.8.0] (2020-12-12) ### Fixed - suppress unwanted warning during `auto` when using npx with npm 7 - temporary fix for installing on Mac with Apple M1 chip, look for x64 versions of node as arm64 not available yet ## [6.7.1] (2020-11-25) ### Fixed - detect and handle a failed download of full archive ([#635]) ## [6.7.0] (2020-07-25) ### Added - `auto` support for: - `.node-version` - `.nvmrc` - `engines` field of `package.json` ## [6.6.0] (2020-07-04) ### Added - labels for node support aliases, such as `lts_latest` ### Fixed - Enable `xz` support by default for macOS 11+ ([#624]) ## [6.5.1] (2020-04-11) ### Added - specify `auto` to read the target version from a `.n-node-version` file (i.e. change filename) ## [6.5.0] (2020-04-11) [YANKED] ### Added - specify `auto` to read the target version from a `.node-version` file ([#616]) ## [6.4.0] (2020-03-10) ### Added - treat `armv8l` as `arm64` ([#614]) ## [6.3.1] (2020-02-25) ### Fixed - remove old version of node before copy to avoid firewall issues on macOS ([#394]) ## [6.3.0] (2020-02-24) ### Added - `--preserve` to preserve npm and npx during install of node ([#587]) ## [6.2.0] (2020-01-29) ### Added - Downloads now default to using tarballs compressed by `xz` over `gzip`, if `xz` support detected. ([#606] [#607]) ## [6.1.3] (2019-11-23) ### Added - added How It Works to README ### Changed - simplified layout for `n doctor` output ## [6.1.2] (2019-11-16) ### Added - advice to reset command hash when node location changes ([#170] [#381] [#451] [#588]) - in README describe raw download of `n` to bootstrap install of node and npm ## [6.1.1] (2019-11-10) ### Fixed - Specify `--no-same-owner` for tarball extraction so cache files not owned by unexpected user (when run with sudo) ([#593]) ## [6.1.0] (2019-10-25) ### Added - deletion of cached versions from menu using 'd' ([#590]) ## [6.0.1] (2019-08-20) ### Fixed - allow options to come after commands, especially `n lsr --all` ## [6.0.0] (2019-08-16) ### Added - version specified using release stream codenames, like `argon` ([#423]) - version specified using nightly et al ([#376]) - `n exec` for running arbitrary command with node and npm in `PATH` ([#185]) - `n run` with legacy aliases of `as` and `use` - `n lsr` for listing matching remote versions, limited to 20 by default ([#383]) - `n doctor` for displaying diagnostic information - `n install` for people used to other products with this command ([#524]) - `--insecure` to disable curl/wget certificate checks - added npm version to installed message ([#210] [#484] [#574]) ### Changed - **Breaking** wget now checks certificates (secure by default, same as curl setup). (#475 #509) - failure messages go to stderr instead of stdout - prefixed `N_NODE_MIRROR` to eventually replace `NODE_MIRROR` - **Breaking** `n ls` now lists local download versions (rather than remote versions) - lookup available versions using `index.tab` rather than screen-scraping (#560) ### Fixed - download errors display informative message, instead of just `Invalid version` ([#482] [#492] et al) - improve reliability of downloads from custom node mirrors, including removing broken `is_oss_ok` ([#560]) - restrict downloads to versions with architecture available ([#463]) ### Removed - **Breaking** support for `PROJECT_NAME` and `PROJECT_URL` for custom downloads ([#342]) ## [5.0.2] (2019-08-02) ### Added - instructions to bottom of menu version selection ## [5.0.1] (2019-07-20) ### Changed - removed reference to prerelease version of v5.0.0 from README ## [5.0.0] (2019-07-20) ### Added - log message after install from cache (previously silent) - extra logging after install if the active and installed node locations are different - support for [NO_COLOR](https://no-color.org) and [CLICOLOR=0](https://bixense.com/clicolors) - suppress progress and colour if not interactive tty - define `N_USE_XZ` to download `.xz` compressed archives instead of `.gz` archives ### Changed - reinstalling active node version always does reinstall (previously silently did nothing) - log message for installing using menu now same format as `npm install` message - updates to GitHub templates and guidelines for contributing et al ## [4.1.0] (2019-05-10) ### Added - 'n uninstall` to remove node and npm - describe `NODE_MIRROR` in `README` ### Removed - `PROJECT_NAME` and `PROJECT_URL` from `README`. First step to deprecating `n project`. Open an issue if you still need this! ## [4.0.0] (2019-05-05) Only minor functional changes, but technically could break scripts relying on specific behaviour. ### Fixed - remove trailing space from `bin` output [#456] ### Added - development tests [#545] ### Changed - internal: improve shell script based on ShellCheck suggestions, quoting variables use etc [#187] [#465] - put single quote marks around parameters to clarify error messages [#485] - update terminology to be more careful with current/latest [#522] ## [3.0.2] (2019-04-07) ### Added - instructions to avoid need for `sudo` when installing to `/usr/local` [#416] [#562] ### Fixed - permission denied errors when running read-only commands without sudo [#416] ## [3.0.1] (2019-04-05) ### Added - install instruction using Homebrew (macOS) [#534] - Table of Contents to README [#466] ### Fixed - lts lookup on node mirrors which don't purge old versions (e.g. taobao) [#512] - hide cursor while selecting version from menu [#528] ### Removed - gitter badge from README, as gitter chatroom inactive - inactive Core Team from README - instructions for scripted install of npm from README, which should no longer be needed and not working on Mac [#536] ## [3.0.0] (2019-03-29) ### Added - detect arm64 architecture [#448][] [#521][] ### Changed - allow `n rm` of active version of node [#541][] [#169][] [#327][] [#441][] - show more version examples in README, including partial version number [#548][] - updated description of interactive version selection [#518][] - make (old) stable an alias for lts [#467][] [#335][] - replace use of `which` with more standard `command -v` [#532][] ### Fixed - error messages when selecting from version menu if active node version not listed [#541][] [#292][] [#367][] [#391][] [#400][] - removed inappropriate `shift` from prune function [#531][] [#529][] ### Removed - Remove old io project support [#516][] [#331][] [#169]: https://github.com/tj/n/issues/169 [#170]: https://github.com/tj/n/issues/170 [#185]: https://github.com/tj/n/issues/185 [#187]: https://github.com/tj/n/issues/187 [#210]: https://github.com/tj/n/issues/210 [#292]: https://github.com/tj/n/issues/292 [#327]: https://github.com/tj/n/issues/327 [#331]: https://github.com/tj/n/issues/331 [#335]: https://github.com/tj/n/issues/335 [#342]: https://github.com/tj/n/issues/342 [#367]: https://github.com/tj/n/issues/367 [#376]: https://github.com/tj/n/issues/376 [#381]: https://github.com/tj/n/issues/381 [#383]: https://github.com/tj/n/issues/383 [#391]: https://github.com/tj/n/issues/391 [#394]: https://github.com/tj/n/issues/394 [#400]: https://github.com/tj/n/issues/400 [#416]: https://github.com/tj/n/issues/416 [#423]: https://github.com/tj/n/issues/423 [#441]: https://github.com/tj/n/issues/441 [#448]: https://github.com/tj/n/issues/448 [#451]: https://github.com/tj/n/issues/451 [#456]: https://github.com/tj/n/issues/456 [#463]: https://github.com/tj/n/issues/463 [#465]: https://github.com/tj/n/issues/465 [#466]: https://github.com/tj/n/issues/466 [#467]: https://github.com/tj/n/issues/467 [#482]: https://github.com/tj/n/issues/482 [#484]: https://github.com/tj/n/issues/484 [#485]: https://github.com/tj/n/issues/485 [#492]: https://github.com/tj/n/issues/492 [#512]: https://github.com/tj/n/issues/512 [#516]: https://github.com/tj/n/issues/516 [#518]: https://github.com/tj/n/issues/518 [#521]: https://github.com/tj/n/issues/521 [#522]: https://github.com/tj/n/issues/522 [#524]: https://github.com/tj/n/issues/524 [#528]: https://github.com/tj/n/issues/528 [#529]: https://github.com/tj/n/issues/529 [#531]: https://github.com/tj/n/issues/531 [#532]: https://github.com/tj/n/issues/532 [#534]: https://github.com/tj/n/issues/534 [#536]: https://github.com/tj/n/issues/536 [#541]: https://github.com/tj/n/issues/541 [#545]: https://github.com/tj/n/issues/545 [#548]: https://github.com/tj/n/issues/548 [#560]: https://github.com/tj/n/issues/560 [#562]: https://github.com/tj/n/issues/562 [#574]: https://github.com/tj/n/issues/574 [#587]: https://github.com/tj/n/issues/587 [#588]: https://github.com/tj/n/issues/588 [#590]: https://github.com/tj/n/issues/590 [#593]: https://github.com/tj/n/issues/593 [#606]: https://github.com/tj/n/issues/606 [#607]: https://github.com/tj/n/issues/607 [#614]: https://github.com/tj/n/issues/614 [#616]: https://github.com/tj/n/issues/616 [#624]: https://github.com/tj/n/issues/624 [#635]: https://github.com/tj/n/pull/635 [#643]: https://github.com/tj/n/pull/643 [#644]: https://github.com/tj/n/pull/644 [#649]: https://github.com/tj/n/issues/649 [#654]: https://github.com/tj/n/issues/654 [#664]: https://github.com/tj/n/pull/664 [#668]: https://github.com/tj/n/pull/668 [#669]: https://github.com/tj/n/pull/669 [#675]: https://github.com/tj/n/pull/675 [#676]: https://github.com/tj/n/pull/676 [#679]: https://github.com/tj/n/issues/679 [#685]: https://github.com/tj/n/issues/685 [#693]: https://github.com/tj/n/issues/693 [#697]: https://github.com/tj/n/issues/697 [#701]: https://github.com/tj/n/issues/701 [#707]: https://github.com/tj/n/issues/707 [#717]: https://github.com/tj/n/issues/717 [#720]: https://github.com/tj/n/issues/720 [#736]: https://github.com/tj/n/pull/736 [#745]: https://github.com/tj/n/pull/745 [#746]: https://github.com/tj/n/pull/746 [#764]: https://github.com/tj/n/pull/764 [#785]: https://github.com/tj/n/pull/785 [#810]: https://github.com/tj/n/pull/810 [#813]: https://github.com/tj/n/pull/813 [#815]: https://github.com/tj/n/pull/815 [#817]: https://github.com/tj/n/pull/817 [#818]: https://github.com/tj/n/pull/818 [#820]: https://github.com/tj/n/pull/820 [#821]: https://github.com/tj/n/pull/821 [#832]: https://github.com/tj/n/pull/832 [Unreleased]: https://github.com/tj/n/compare/master...develop [10.2.0]: https://github.com/tj/n/compare/v10.1.0...v10.2.0 [10.1.0]: https://github.com/tj/n/compare/v10.0.0...v10.1.0 [10.0.0]: https://github.com/tj/n/compare/v9.2.3...v10.0.0 [9.2.3]: https://github.com/tj/n/compare/v9.2.2...v9.2.3 [9.2.2]: https://github.com/tj/n/compare/v9.2.1...v9.2.2 [9.2.1]: https://github.com/tj/n/compare/v9.2.0...v9.2.1 [9.2.0]: https://github.com/tj/n/compare/v9.1.0...v9.2.0 [9.1.0]: https://github.com/tj/n/compare/v9.0.1...v9.1.0 [9.0.1]: https://github.com/tj/n/compare/v9.0.0...v9.0.1 [9.0.0]: https://github.com/tj/n/compare/v8.2.0...v9.0.0 [8.2.0]: https://github.com/tj/n/compare/v8.1.0...v8.2.0 [8.1.0]: https://github.com/tj/n/compare/v8.0.2...v8.1.0 [8.0.2]: https://github.com/tj/n/compare/v8.0.1...v8.0.2 [8.0.1]: https://github.com/tj/n/compare/v8.0.0...v8.0.1 [8.0.0]: https://github.com/tj/n/compare/v7.5.0...v8.0.0 [7.5.0]: https://github.com/tj/n/compare/v7.4.1...v7.5.0 [7.4.1]: https://github.com/tj/n/compare/v7.4.0...v7.4.1 [7.4.0]: https://github.com/tj/n/compare/v7.3.1...v7.4.0 [7.3.1]: https://github.com/tj/n/compare/v7.3.0...v7.3.1 [7.3.0]: https://github.com/tj/n/compare/v7.2.2...v7.3.0 [7.2.2]: https://github.com/tj/n/compare/v7.2.1...v7.2.2 [7.2.1]: https://github.com/tj/n/compare/v7.1.0...v7.2.1 [7.2.0]: https://github.com/tj/n/compare/v7.1.0...v7.2.0 [7.1.0]: https://github.com/tj/n/compare/v7.0.2...v7.1.0 [7.0.2]: https://github.com/tj/n/compare/v7.0.1...v7.0.2 [7.0.1]: https://github.com/tj/n/compare/v7.0.0...v7.0.1 [7.0.0]: https://github.com/tj/n/compare/v6.8.0...v7.0.0 [6.8.0]: https://github.com/tj/n/compare/v6.7.1...v6.8.0 [6.7.1]: https://github.com/tj/n/compare/v6.7.0...v6.7.1 [6.7.0]: https://github.com/tj/n/compare/v6.6.0...v6.7.0 [6.6.0]: https://github.com/tj/n/compare/v6.5.1...v6.6.0 [6.5.1]: https://github.com/tj/n/compare/v6.5.0...v6.5.1 [6.5.0]: https://github.com/tj/n/compare/v6.4.0...v6.5.0 [6.4.0]: https://github.com/tj/n/compare/v6.3.1...v6.4.0 [6.3.1]: https://github.com/tj/n/compare/v6.3.0...v6.3.1 [6.3.0]: https://github.com/tj/n/compare/v6.2.0...v6.3.0 [6.2.0]: https://github.com/tj/n/compare/v6.1.3...v6.2.0 [6.1.3]: https://github.com/tj/n/compare/v6.0.2...v6.1.3 [6.1.2]: https://github.com/tj/n/compare/v6.0.1...v6.1.2 [6.1.1]: https://github.com/tj/n/compare/v6.0.0...v6.1.1 [6.1.0]: https://github.com/tj/n/compare/v6.0.1...v6.1.0 [6.0.1]: https://github.com/tj/n/compare/v6.0.0...v6.0.1 [6.0.0]: https://github.com/tj/n/compare/v5.0.2...v6.0.0 [5.0.2]: https://github.com/tj/n/compare/v5.0.1...v5.0.2 [5.0.1]: https://github.com/tj/n/compare/v5.0.0...v5.0.1 [5.0.0]: https://github.com/tj/n/compare/v4.1.0...v5.0.0 [4.1.0]: https://github.com/tj/n/compare/v4.0.0...v4.1.0 [4.0.0]: https://github.com/tj/n/compare/v3.0.2...v4.0.0 [3.0.2]: https://github.com/tj/n/compare/v3.0.1...v3.0.2 [3.0.1]: https://github.com/tj/n/compare/v3.0.0...v3.0.1 [3.0.0]: https://github.com/tj/n/compare/v2.1.12...v3.0.0 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to n ## Issues Before opening up an issue, please search for previous reports. New issues are welcome, whether questions or suggestions or reporting bugs. You are also welcome to contribute by adding helpful comments on an existing issue. ## Pull Requests Pull Requests will be considered. Please open an issue to discuss your idea before requesting big changes. Please submit pull requests against the `develop` branch. The template will prompt you for the details, such as what problem you are solving, and relevant issue numbers. Don't change the version number or CHANGELOG, as they are updated by maintainers as the release is being prepared. ## Code of Conduct Hate speech of any kind is not tolerated. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 TJ Holowaychuk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ PREFIX ?= /usr/local install: bin/n mkdir -p "$(PREFIX)/bin" cp bin/n "$(PREFIX)/bin/n" uninstall: rm -f "$(PREFIX)/bin/n" .PHONY: install uninstall ================================================ FILE: README.md ================================================ # `n` – Interactively Manage Your Node.js Versions [![npm](https://img.shields.io/npm/dt/n.svg?style=flat-square)](https://www.npmjs.com/package/n) [![npm](https://img.shields.io/npm/dm/n.svg?style=flat-square)](https://www.npmjs.com/package/n) [![npm](https://img.shields.io/npm/v/n.svg?style=flat-square)](https://www.npmjs.com/package/n) [![npm](https://img.shields.io/npm/l/n.svg?style=flat-square)](https://www.npmjs.com/package/n) Node.js version management: no subshells, no profile setup, no convoluted API, just **simple**. ![usage animation](https://nimit.io/images/n/n.gif) - [`n` – Interactively Manage Your Node.js Versions](#n--interactively-manage-your-nodejs-versions) - [Supported Platforms](#supported-platforms) - [Installation](#installation) - [Third Party Installers](#third-party-installers) - [Replacing a previous node install](#replacing-a-previous-node-install) - [Installing Node.js Versions](#installing-nodejs-versions) - [Specifying Node.js Versions](#specifying-nodejs-versions) - [Removing Versions](#removing-versions) - [Using Downloaded Node.js Versions Without Reinstalling](#using-downloaded-nodejs-versions-without-reinstalling) - [Preserving npm](#preserving-npm) - [Miscellaneous](#miscellaneous) - [Custom Mirror](#custom-mirror) - [Custom Architecture](#custom-architecture) - [Optional Environment Variables](#optional-environment-variables) - [How It Works](#how-it-works) ## Supported Platforms `n` is supported on macOS, Linux, including with Windows Subsystem for Linux, and various other unix-like systems. It is written as a BASH script but does not require you to use BASH as your command shell. `n` does not work in native shells on Microsoft Windows (like PowerShell), or Git for Windows BASH, or with the Cygwin DLL. ## Installation If you already have Node.js installed, an easy way to install `n` is using `npm`: npm install -g n The default root location used when running `n` is `/usr/local` where a normal user does not have write permission. You may strike the same sort of permission error when using npm to install global modules, like the above command. You have three main options: 1) change the ownership of the relevant directories to yourself (see below) 2) tell `n` to use a custom location where you do have write permissions (see `N_PREFIX`) 3) put `sudo` in front of the command to run it as super user `n` caches Node.js versions in subdirectory `n/versions`. The _active_ Node.js version is installed in subdirectories `bin`, `include`, `lib`, and `share`. To take ownership of the system directories (option 1): # make cache folder (if missing) and take ownership sudo mkdir -p /usr/local/n sudo chown -R $(whoami) /usr/local/n # make sure the required folders exist (safe to execute even if they already exist) sudo mkdir -p /usr/local/bin /usr/local/lib /usr/local/include /usr/local/share # take ownership of Node.js install destination folders sudo chown -R $(whoami) /usr/local/bin /usr/local/lib /usr/local/include /usr/local/share ----- If `npm` is not yet available, one way to bootstrap an install is to download and run `n` directly. To install the `lts` version of Node.js: curl -fsSL https://raw.githubusercontent.com/tj/n/master/bin/n | bash -s install lts # If you want n installed, you can use npm now. npm install -g n If you don't need support for updates to `n` itself you can just save the download: curl -fsSL -o /usr/local/bin/n https://raw.githubusercontent.com/tj/n/master/bin/n chmod 0755 /usr/local/bin/n n install lts ### Third Party Installers On macOS with [Homebrew](https://brew.sh/) you can install the [n formula](https://formulae.brew.sh/formula/n). brew install n Or on macOS with [MacPorts](https://www.macports.org/) you can install the [n port](https://ports.macports.org/port/n/summary): port install n On Linux and macOS, [n-install](https://github.com/mklement0/n-install) allows installation directly from GitHub; for instance: curl -L https://bit.ly/n-install | bash n-install sets both `PREFIX` and `N_PREFIX` to `$HOME/n`, installs `n` to `$HOME/n/bin`, modifies the initialization files of supported shells to export `N_PREFIX` and add `$HOME/n/bin` to the `PATH`, and installs the latest LTS Node.js version. As a result, both `n` itself and all Node.js versions it manages are hosted inside a single, optionally configurable directory, which you can later remove with the included `n-uninstall` script. `n-update` updates `n` itself to the latest version. See the [n-install repo](https://github.com/mklement0/n-install) for more details. ### Replacing a previous node install Changing from a previous Node.js installed to a different location may involve a few extra steps. See docs for [changing node location](./docs/changing-node-location.md) for a walk-through example of switching from using Homebrew to using `n` to manage Node.js. You have a problem with multiple versions if after installing node you see the "installed" and "active" locations are different: ```console % n lts copying : node/20.12.2 installed : v20.12.2 to /usr/local/bin/node active : v21.7.3 at /opt/homebrew/bin/node ``` ## Installing Node.js Versions Simply execute `n ` to download and install a version of Node.js. If `` has already been downloaded, `n` will install from its cache. n 10.16.0 n lts Execute `n` on its own to view your downloaded versions, and install the selected version. $ n node/4.9.1 ο node/8.11.3 node/10.15.0 Use up/down arrow keys to select a version, return key to install, d to delete, q to quit (You can also use j and k to select next or previous version instead of using arrows, or ctrl+n and ctrl+p.) If the active node version does not change after install, try opening a new shell in case seeing a stale version. ## Specifying Node.js Versions There are a variety of ways of specifying the target Node.js version for `n` commands. Most commands use the latest matching version, and `n ls-remote` lists multiple matching versions. Numeric version numbers can be complete or incomplete, with an optional leading `v`. - `4.9.1` - `8`: 8.x.y versions - `v6.1`: 6.1.x versions There are labels for two especially useful versions: - `lts`: newest Long Term Support official release - `latest`, `current`: newest official release There is an `auto` label to read the target version from a file in the current directory, or any parent directory. `n` looks for in order: - `.n-node-version`: version on single line. Custom to `n`. - `.node-version`: version on single line. Used by multiple tools: [node-version-usage](https://github.com/shadowspawn/node-version-usage) - `.nvmrc`: version on single line. Used by `nvm`. - if no version file found, look for `engine` as below. The `engine` label looks for a `package.json` file and reads the `engines` field to determine compatible Node.js. Requires an installed version of `jq` or `node`, and uses `npx semver` to resolve complex ranges. There is support for the named release streams: - `argon`, `boron`, `carbon`: codenames for LTS release streams These Node.js support aliases may be used, although simply resolve to the latest matching version: - `active`, `lts_active`, `lts_latest`, `lts`, `current`, `supported` The last version form is for specifying [other releases](https://nodejs.org/download) available using the name of the remote download folder optionally followed by the complete or incomplete version. - `nightly` - `test/v11.0.0-test20180528` - `rc/10` ## Removing Versions Remove some cached versions: n rm 0.9.4 v0.10.0 Removing all cached versions except the installed version: n prune Remove the installed Node.js (does not affect the cached versions). This can be useful to revert to the system version of node (if in a different location), or if you no longer wish to use node and npm, or are switching to a different way of managing them. n uninstall ## Using Downloaded Node.js Versions Without Reinstalling There are three commands for working directly with your downloaded versions of Node.js, without reinstalling. You can show the path to the downloaded `node` version: $ n which 6.14.3 /usr/local/n/versions/6.14.3/bin/node Or run a downloaded `node` version with the `n run` command: n run 8.11.3 --debug some.js Or execute a command with `PATH` modified so `node` and `npm` will be from the downloaded Node.js version. (NB: `npm` run this way will be using global node_modules from the target node version folder.) n exec 10 my-script --fast test n exec lts zsh ## Preserving npm A Node.js install normally also includes `npm`, `npx`, and `corepack`, but you may wish to preserve your current (especially newer) versions using `--preserve`: $ npm install -g npm@latest ... $ npm --version 6.13.7 # Node.js 8.17.0 includes (older) npm 6.13.4 $ n -p 8 installed : v8.17.0 $ npm --version 6.13.7 You can make this the default by setting the environment variable to a non-empty string. There are separate environment variables for `npm` and `corepack`: export N_PRESERVE_NPM=1 export N_PRESERVE_COREPACK=1 You can be explicit to get the desired behaviour whatever the environment variables: n --preserve nightly n --no-preserve latest ## Miscellaneous Command line help can be obtained from `n --help`. List matching remote versions available for download: n ls-remote lts n ls-remote latest n lsr 10 n --all lsr List downloaded versions in cache: n ls Download version into cache: n download 22 Use `n` to access cached versions (already downloaded) without internet available. n --offline 12 Remove the cache version after installing using `--cleanup`. This is particularly useful for a one-shot install, like in a docker container. curl -fsSL https://raw.githubusercontent.com/tj/n/master/bin/n | bash -s install --cleanup lts Normally `n run`, `n exec`, and `n which` will fail if the target version is not already in the cache. You can add `--download` to use the cache if available or download if required: n --download run 18.3 my-script.js Display diagnostics to help resolve problems: n doctor ## Custom Mirror If you would like to use a different Node.js mirror which has the same layout as the default , you can define `N_NODE_MIRROR`. One example is for users in China who can define: export N_NODE_MIRROR=https://npmmirror.com/mirrors/node Another example is the Node.js [unofficial-builds project](https://github.com/nodejs/unofficial-builds/) which has downloads for some platforms not made available officially, such as armv6l (Raspberry Pi) and 32-bit x86. export N_NODE_MIRROR=https://unofficial-builds.nodejs.org/download/release You may need to specify the architecture explicitly if not autodetected by `n`, such as using `musl` `libc` on Alpine. You can do that with `N_ARCH` or `--arch`: export N_NODE_MIRROR=https://unofficial-builds.nodejs.org/download/release export N_ARCH=x64-musl apk add bash curl libstdc++ n install lts If the custom mirror requires authentication you can add the [url-encoded](https://urlencode.org) username and password into the URL. e.g. export N_NODE_MIRROR=https://encoded-username:encoded-password@host:port/path There is also `N_NODE_DOWNLOAD_MIRROR` for a different mirror with same layout as the default . ## Custom Architecture By default `n` picks the binaries matching your system architecture. For example, on a 64 bit system `n` will download 64 bit binaries. On a Mac with Apple silicon: - for Node.js 16 and higher, `n` defaults to arm64 binaries which run natively - for older versions of Node.js, `n` defaults to x64 binaries which run in Rosetta 2 You can override the default architecture by using the `-a` or `--arch` option, or set `N_ARCH` environment variable. e.g. reinstall latest version of Node.js with x64 binaries: n rm current n --arch x64 current ## Optional Environment Variables The `n` command downloads and installs to `/usr/local` by default, but you may override this location by defining `N_PREFIX`. To change the location to say `$HOME/.n`, add lines like the following to your shell initialization file: export N_PREFIX=$HOME/.n export PATH=$N_PREFIX/bin:$PATH If you want to store the downloads under a different location, use `N_CACHE_PREFIX`. This does _not_ affect where the active node version is installed. `n` defaults to using xz compressed Node.js tarballs for the download if it is likely tar on the system supports xz decompression. You can override the automatic choice by setting an environment variable to zero or non-zero: export N_USE_XZ=0 # to disable export N_USE_XZ=1 # to enable You can be explicit to get the desired behaviour whatever the environment variable: n install --use-xz nightly n install --no-use-xz latest In brief: - `N_NODE_MIRROR`: see [Custom Mirror](#custom-mirror) - `N_NODE_DOWNLOAD_MIRROR`: see [Custom Mirror](#custom-mirror) - support for [NO_COLOR](https://no-color.org) and [CLICOLOR=0](https://bixense.com/clicolors) for controlling use of ANSI color codes - `N_MAX_REMOTE_MATCHES` to change the default `ls-remote` maximum of 20 matching versions - `N_PRESERVE_NPM`: see [Preserving npm](#preserving-npm) - `N_PRESERVE_COREPACK`: see [Preserving npm](#preserving-npm) - `N_ARCH`: see [Custom Architecture](#custom-architecture) ## How It Works `n` downloads a prebuilt Node.js package and installs to a single prefix (e.g. `/usr/local`). This overwrites the previous version. The `bin` folder in this location should be in your `PATH` (e.g. `/usr/local/bin`). The downloads are kept in a cache folder to be used for reinstalls. The downloads are also available for limited use using `n which` and `n run` and `n exec`. The global `npm` packages are not changed by the install, with the exception of `npm` itself which is part of the Node.js install. ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions The latest two major versions get security updates. Pull Requests for security issues will be considered for older versions. ## Reporting a Vulnerability To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ================================================ FILE: bin/dev/release ================================================ #!/usr/bin/env bash # Run this from the develop branch after setting the version number in bin/n, # when ready to release apart from copying up to master and tagging. { # force scan of whole file into memory so not affected by changing branches and self-modifying. # # confirm # function confirm { read -p "$1 " -r if [[ ! "${REPLY}" =~ ^[Yy]$ ]]; then echo "Stopping" exit 2 fi } readonly N_VERSION="$(./bin/n --version)" if [[ "${N_VERSION}" =~ ^[0-9.]+-[0-9]+$ ]]; then echo "Error: internal version number still prerelease, set it to desired version first." exit 2 fi confirm "Are you releasing version '${N_VERSION}' ?" confirm "Have you updated CHANGELOG?" confirm "Are you running this from a shell with full internet access (not through proxy to run tests)?" git checkout master git merge develop npm version "${N_VERSION}" read -p "One Time Password for npm publish: " -r set -x npm publish . --otp "${REPLY}" git push --follow-tags git checkout develop git merge master npm version -no-git-tag-version prepatch set +x echo "Reminder: update the internal number to match." echo "Reminder: add release description to github (from CHANGELOG)" exit } ================================================ FILE: bin/n ================================================ #!/usr/bin/env bash # shellcheck disable=SC2155 # Disabled "Declare and assign separately to avoid masking return values": https://github.com/koalaman/shellcheck/wiki/SC2155 # # log # log() { printf " ${SGR_CYAN}%10s${SGR_RESET} : ${SGR_FAINT}%s${SGR_RESET}\n" "$1" "$2" } # # verbose_log # Can suppress with --quiet. # Like log but to stderr rather than stdout, so can also be used from "display" routines. # verbose_log() { if [[ "${SHOW_VERBOSE_LOG}" == "true" ]]; then >&2 printf " ${SGR_CYAN}%10s${SGR_RESET} : ${SGR_FAINT}%s${SGR_RESET}\n" "$1" "$2" fi } # # Exit with the given # abort() { >&2 printf "\n ${SGR_RED}Error: %s${SGR_RESET}\n\n" "$*" && exit 1 } # # Synopsis: trace message ... # Debugging output to stderr, not used in production code. # function trace() { >&2 printf "trace: %s\n" "$*" } # # Synopsis: echo_red message ... # Highlight message in colour (on stdout). # function echo_red() { printf "${SGR_RED}%s${SGR_RESET}\n" "$*" } # # Synopsis: n_grep # grep wrapper to ensure consistent grep options and circumvent aliases. # function n_grep() { GREP_OPTIONS='' command grep "$@" } # # Setup and state # VERSION="10.2.0" N_PREFIX="${N_PREFIX-/usr/local}" N_PREFIX=${N_PREFIX%/} readonly N_PREFIX N_CACHE_PREFIX="${N_CACHE_PREFIX-${N_PREFIX}}" N_CACHE_PREFIX=${N_CACHE_PREFIX%/} CACHE_DIR="${N_CACHE_PREFIX}/n/versions" readonly N_CACHE_PREFIX CACHE_DIR N_NODE_MIRROR=${N_NODE_MIRROR:-${NODE_MIRROR:-https://nodejs.org/dist}} N_NODE_MIRROR=${N_NODE_MIRROR%/} readonly N_NODE_MIRROR N_NODE_DOWNLOAD_MIRROR=${N_NODE_DOWNLOAD_MIRROR:-https://nodejs.org/download} N_NODE_DOWNLOAD_MIRROR=${N_NODE_DOWNLOAD_MIRROR%/} readonly N_NODE_DOWNLOAD_MIRROR # Using xz instead of gzip is enabled by default, if xz compatibility checks pass. # User may set N_USE_XZ to 0 to disable, or set to anything else to enable. # May also be overridden by command line flags. # Normalise external values to true/false if [[ "${N_USE_XZ}" = "0" ]]; then N_USE_XZ="false" elif [[ -n "${N_USE_XZ+defined}" ]]; then N_USE_XZ="true" fi # Not setting to readonly. Overriden by CLI flags, and update_xz_settings_for_version. N_MAX_REMOTE_MATCHES=${N_MAX_REMOTE_MATCHES:-20} # modified by update_mirror_settings_for_version g_mirror_url=${N_NODE_MIRROR} g_mirror_folder_name="node" # Options for curl and wget. # Defining commands in variables is fraught (https://mywiki.wooledge.org/BashFAQ/050) # but we can follow the simple case and store arguments in an array. GET_SHOWS_PROGRESS="false" # --location to follow redirects # --fail to avoid happily downloading error page from web server for 404 et al # --show-error to show why failed (on stderr) CURL_OPTIONS=( "--location" "--fail" "--show-error" ) if [[ -t 1 ]]; then CURL_OPTIONS+=( "--progress-bar" ) command -v curl &> /dev/null && GET_SHOWS_PROGRESS="true" else CURL_OPTIONS+=( "--silent" ) fi WGET_OPTIONS=( "-q" "-O-" ) # Legacy support using unprefixed env. No longer documented in README. if [ -n "$HTTP_USER" ];then if [ -z "$HTTP_PASSWORD" ]; then abort "Must specify HTTP_PASSWORD when supplying HTTP_USER" fi CURL_OPTIONS+=( "-u $HTTP_USER:$HTTP_PASSWORD" ) WGET_OPTIONS+=( "--http-password=$HTTP_PASSWORD" "--http-user=$HTTP_USER" ) elif [ -n "$HTTP_PASSWORD" ]; then abort "Must specify HTTP_USER when supplying HTTP_PASSWORD" fi # Set by set_active_node g_active_node= # set by various lookups to allow mixed logging and return value from function, especially for engine and node g_target_node= DOWNLOAD=false # set to opt-out of activate (install), and opt-in to download (run, exec) CLEANUP=false # remove cached download after install ARCH="${N_ARCH-}" SHOW_VERBOSE_LOG="true" OFFLINE=false # ANSI escape codes # https://en.wikipedia.org/wiki/ANSI_escape_code # https://no-color.org # https://bixense.com/clicolors USE_COLOR="true" if [[ -n "${CLICOLOR_FORCE+defined}" && "${CLICOLOR_FORCE}" != "0" ]]; then USE_COLOR="true" elif [[ -n "${NO_COLOR+defined}" || "${CLICOLOR}" = "0" || ! -t 1 ]]; then USE_COLOR="false" fi readonly USE_COLOR # Select Graphic Rendition codes if [[ "${USE_COLOR}" = "true" ]]; then # KISS and use codes rather than tput, avoid dealing with missing tput or TERM. readonly SGR_RESET="\033[0m" readonly SGR_FAINT="\033[2m" readonly SGR_RED="\033[31m" readonly SGR_CYAN="\033[36m" else readonly SGR_RESET= readonly SGR_FAINT= readonly SGR_RED= readonly SGR_CYAN= fi # # set_arch to override $(uname -a) # set_arch() { if test -n "$1"; then ARCH="$1" else abort "missing -a|--arch value" fi } # # Synopsis: set_insecure # Globals modified: # - CURL_OPTIONS # - WGET_OPTIONS # function set_insecure() { CURL_OPTIONS+=( "--insecure" ) WGET_OPTIONS+=( "--no-check-certificate" ) } # # Synposis: display_major_version numeric-version # display_major_version() { local version=$1 version="${version#v}" version="${version%%.*}" echo "${version}" } display_masked_url() { echo "$1" | sed -r 's/(https?:\/\/[^:]+):([^@]+)@/\1:****@/' } # # Synopsis: update_mirror_settings_for_version version # e.g. means using download mirror and folder is nightly # Globals modified: # - g_mirror_url # - g_mirror_folder_name # function update_mirror_settings_for_version() { if is_download_folder "$1" ; then g_mirror_folder_name="$1" g_mirror_url="${N_NODE_DOWNLOAD_MIRROR}/${g_mirror_folder_name}" elif is_download_version "$1"; then [[ "$1" =~ ^([^/]+)/(.*) ]] local remote_folder="${BASH_REMATCH[1]}" g_mirror_folder_name="${remote_folder}" g_mirror_url="${N_NODE_DOWNLOAD_MIRROR}/${g_mirror_folder_name}" fi } # # Synopsis: update_xz_settings_for_version numeric-version # Globals modified: # - N_USE_XZ # function update_xz_settings_for_version() { # tarballs in xz format were available in later version of iojs, but KISS and only use xz from v4. if [[ "${N_USE_XZ}" = "true" ]]; then local major_version="$(display_major_version "$1")" if [[ "${major_version}" -lt 4 ]]; then N_USE_XZ="false" fi fi } # # Synopsis: update_arch_settings_for_version numeric-version # Globals modified: # - ARCH # function update_arch_settings_for_version() { local tarball_platform="$(display_tarball_platform)" if [[ -z "${ARCH}" && "${tarball_platform}" = "darwin-arm64" ]]; then # First native builds were for v16, but can use x64 in rosetta for older versions. local major_version="$(display_major_version "$1")" if [[ "${major_version}" -lt 16 ]]; then ARCH=x64 fi fi } # # Synopsis: is_lts_codename version # function is_lts_codename() { # https://github.com/nodejs/Release/blob/master/CODENAMES.md # e.g. argon, Boron [[ "$1" =~ ^([Aa]rgon|[Bb]oron|[Cc]arbon|[Dd]ubnium|[Ee]rbium|[Ff]ermium|[Gg]allium|[Hh]ydrogen|[Ii]ron|[Jj]od|[Kk]rypton|[Ll]ithium)$ ]] } # # Synopsis: is_download_folder version # function is_download_folder() { # e.g. nightly [[ "$1" =~ ^(next-nightly|nightly|rc|release|test|v8-canary)$ ]] } # # Synopsis: is_download_version version # function is_download_version() { # e.g. nightly/, nightly/latest, nightly/v11 if [[ "$1" =~ ^([^/]+)/(.*) ]]; then local remote_folder="${BASH_REMATCH[1]}" is_download_folder "${remote_folder}" return fi return 2 } # # Synopsis: is_numeric_version version # function is_numeric_version() { # e.g. 6, v7.1, 8.11.3 [[ "$1" =~ ^[v]{0,1}[0-9]+(\.[0-9]+){0,2}$ ]] } # # Synopsis: is_exact_numeric_version version # function is_exact_numeric_version() { # e.g. 6, v7.1, 8.11.3 [[ "$1" =~ ^[v]{0,1}[0-9]+\.[0-9]+\.[0-9]+$ ]] } # # Synopsis: is_node_support_version version # Reference: https://github.com/nodejs/package-maintenance/issues/236#issue-474783582 # function is_node_support_version() { [[ "$1" =~ ^(active|lts_active|lts_latest|lts|current|supported)$ ]] } # # Synopsis: display_latest_node_support_alias version # Map aliases onto existing n aliases, current and lts # function display_latest_node_support_alias() { case "$1" in "active") printf "current" ;; "lts_active") printf "lts" ;; "lts_latest") printf "lts" ;; "lts") printf "lts" ;; "current") printf "current" ;; "supported") printf "current" ;; *) printf "unexpected-version" esac } # # Functions used when showing versions installed # enter_fullscreen() { # Set cursor to be invisible tput civis 2> /dev/null # Save screen contents tput smcup 2> /dev/null stty -echo } leave_fullscreen() { # Set cursor to normal tput cnorm 2> /dev/null # Restore screen contents tput rmcup 2> /dev/null stty echo } handle_sigint() { leave_fullscreen S="$?" kill 0 exit $S } handle_sigtstp() { leave_fullscreen kill -s SIGSTOP $$ } # # Output usage information. # display_help() { cat <<-EOF Usage: n [options] [COMMAND] [args] Commands: n Display downloaded Node.js versions and install selection n latest Install the latest Node.js release (downloading if necessary) n lts Install the latest LTS Node.js release (downloading if necessary) n Install Node.js (downloading if necessary) n install Install Node.js (downloading if necessary) n run [args ...] Execute downloaded Node.js with [args ...] n which Output path for downloaded node n exec [args...] Execute command with modified PATH, so downloaded node and npm first n rm Remove the given downloaded version(s) n prune Remove all downloaded versions except the installed version n --latest Output the latest Node.js version available n --lts Output the latest LTS Node.js version available n ls Output downloaded versions n ls-remote [version] Output matching versions available for download n uninstall Remove the installed Node.js n download Download Node.js into cache Options: -V, --version Output version of n -h, --help Display help information -p, --preserve Preserve npm and npx during install of Node.js -q, --quiet Disable curl output. Disable log messages processing "auto" and "engine" labels. -d, --download Download if necessary. Used with run/exec/which. --cleanup Remove cached version after install -a, --arch Override system architecture --offline Resolve target version against cached downloads instead of internet lookup --all ls-remote displays all matches instead of last 20 --insecure Turn off certificate checking for https requests (may be needed from behind a proxy server) --use-xz/--no-use-xz Override automatic detection of xz support and enable/disable use of xz compressed node downloads. Aliases: install: i latest: current ls: list lsr: ls-remote lts: stable rm: - run: use, as which: bin Versions: Numeric version numbers can be complete or incomplete, with an optional leading 'v'. Versions can also be specified by label, or codename, and other downloadable releases by / 4.9.1, 8, v6.1 Numeric versions lts Newest Long Term Support official release latest, current Newest official release auto Read version from file: .n-node-version, .node-version, .nvmrc, or package.json engine Read version from package.json boron, carbon Codenames for release streams lts_latest Node.js support aliases and nightly, rc/10 et al EOF } err_no_installed_print_help() { display_help abort "no downloaded versions yet, see above help for commands" } # # Synopsis: next_version_installed selected_version # Output version after selected (which may be blank under some circumstances). # function next_version_installed() { display_cache_versions | n_grep "$1" -A 1 | tail -n 1 } # # Synopsis: prev_version_installed selected_version # Output version before selected (which may be blank under some circumstances). # function prev_version_installed() { display_cache_versions | n_grep "$1" -B 1 | head -n 1 } # # Output n version. # display_n_version() { echo "$VERSION" && exit 0 } # # Synopsis: set_active_node # Checks cached downloads for a binary matching the active node. # Globals modified: # - g_active_node # function set_active_node() { g_active_node= local node_path="$(command -v node)" if [[ -x "${node_path}" ]]; then local installed_version=$(node --version) installed_version=${installed_version#v} for dir in "${CACHE_DIR}"/*/ ; do local folder_name="${dir%/}" folder_name="${folder_name##*/}" if diff &> /dev/null \ "${CACHE_DIR}/${folder_name}/${installed_version}/bin/node" \ "${node_path}" ; then g_active_node="${folder_name}/${installed_version}" break fi done fi } # # Display sorted versions directories paths. # display_versions_paths() { find "$CACHE_DIR" -maxdepth 2 -type d \ | sed 's|'"$CACHE_DIR"'/||g' \ | n_grep -E "/[0-9]+\.[0-9]+\.[0-9]+" \ | sed 's|/|.|' \ | sort -k 1,1 -k 2,2n -k 3,3n -k 4,4n -t . \ | sed 's|\.|/|' } # # Display installed versions with # display_versions_with_selected() { local selected="$1" echo for version in $(display_versions_paths); do if test "$version" = "$selected"; then printf " ${SGR_CYAN}ο${SGR_RESET} %s\n" "$version" else printf " ${SGR_FAINT}%s${SGR_RESET}\n" "$version" fi done echo printf "Use up/down arrow keys to select a version, return key to install, d to delete, q to quit" } # # Synopsis: display_cache_versions # function display_cache_versions() { for folder_and_version in $(display_versions_paths); do echo "${folder_and_version}" done } # # Display current node --version and others installed. # menu_select_cache_versions() { enter_fullscreen set_active_node local selected="${g_active_node}" clear display_versions_with_selected "${selected}" trap handle_sigint INT trap handle_sigtstp SIGTSTP ESCAPE_SEQ=$'\033' UP=$'A' DOWN=$'B' CTRL_P=$'\020' CTRL_N=$'\016' while true; do read -rsn 1 key case "$key" in "$ESCAPE_SEQ") # Handle ESC sequences followed by other characters, i.e. arrow keys read -rsn 1 -t 1 tmp # See "[" if terminal in normal mode, and "0" in application mode if [[ "$tmp" == "[" || "$tmp" == "O" ]]; then read -rsn 1 -t 1 arrow case "$arrow" in "$UP") clear selected="$(prev_version_installed "${selected}")" display_versions_with_selected "${selected}" ;; "$DOWN") clear selected="$(next_version_installed "${selected}")" display_versions_with_selected "${selected}" ;; esac fi ;; "d") if [[ -n "${selected}" ]]; then clear # Note: prev/next is constrained to min/max local after_delete_selection="$(next_version_installed "${selected}")" if [[ "${after_delete_selection}" == "${selected}" ]]; then after_delete_selection="$(prev_version_installed "${selected}")" fi remove_versions "${selected}" if [[ "${after_delete_selection}" == "${selected}" ]]; then clear leave_fullscreen echo "All downloaded versions have been deleted from cache." exit fi selected="${after_delete_selection}" display_versions_with_selected "${selected}" fi ;; # Vim or Emacs 'up' key "k"|"$CTRL_P") clear selected="$(prev_version_installed "${selected}")" display_versions_with_selected "${selected}" ;; # Vim or Emacs 'down' key "j"|"$CTRL_N") clear selected="$(next_version_installed "${selected}")" display_versions_with_selected "${selected}" ;; "q") clear leave_fullscreen exit ;; "") # enter key returns empty string leave_fullscreen [[ -n "${selected}" ]] && activate "${selected}" exit ;; esac done } # # Move up a line and erase. # erase_line() { printf "\033[1A\033[2K" } # # Disable PaX mprotect for # disable_pax_mprotect() { test -z "$1" && abort "binary required" local binary="$1" # try to disable mprotect via XATTR_PAX header local PAXCTL="$(PATH="/sbin:/usr/sbin:$PATH" command -v paxctl-ng 2>&1)" local PAXCTL_ERROR=1 if [ -x "$PAXCTL" ]; then $PAXCTL -l && $PAXCTL -m "$binary" >/dev/null 2>&1 PAXCTL_ERROR="$?" fi # try to disable mprotect via PT_PAX header if [ "$PAXCTL_ERROR" != 0 ]; then PAXCTL="$(PATH="/sbin:/usr/sbin:$PATH" command -v paxctl 2>&1)" if [ -x "$PAXCTL" ]; then $PAXCTL -Cm "$binary" >/dev/null 2>&1 fi fi } # # clean_copy_folder # clean_copy_folder() { local source="$1" local target="$2" if [[ -d "${source}" ]]; then rm -rf "${target}" cp -fR "${source}" "${target}" fi } # # Activate # activate() { local version="$1" local dir="$CACHE_DIR/$version" local original_node="$(command -v node)" local installed_node="${N_PREFIX}/bin/node" log "copying" "$version" # Ideally we would just copy from cache to N_PREFIX, but there are some complications # - various linux versions use symlinks for folders in /usr/local and also error when copy folder onto symlink # - we have used cp for years, so keep using it for backwards compatibility (instead of say rsync) # - we allow preserving npm # - we want to be somewhat robust to changes in tarball contents, so use find instead of hard-code expected subfolders # # This code was purist and concise for a long time. # Now twice as much code, but using same code path for all uses, and supporting more setups. # Copy lib before bin so symlink targets exist. # lib mkdir -p "$N_PREFIX/lib" # Copy everything except node_modules. find "$dir/lib" -mindepth 1 -maxdepth 1 \! -name node_modules -exec cp -fR "{}" "$N_PREFIX/lib" \; if [[ -z "${N_PRESERVE_NPM}" ]]; then mkdir -p "$N_PREFIX/lib/node_modules" # Copy just npm, skipping possible added global modules after download. Clean copy to avoid version change problems. clean_copy_folder "$dir/lib/node_modules/npm" "$N_PREFIX/lib/node_modules/npm" fi # Takes same steps for corepack (experimental in node 16.9.0) as for npm, to avoid version problems. if [[ -e "$dir/lib/node_modules/corepack" && -z "${N_PRESERVE_COREPACK}" ]]; then mkdir -p "$N_PREFIX/lib/node_modules" clean_copy_folder "$dir/lib/node_modules/corepack" "$N_PREFIX/lib/node_modules/corepack" fi # bin mkdir -p "$N_PREFIX/bin" # Remove old node to avoid potential problems with firewall getting confused on Darwin by overwrite. rm -f "$N_PREFIX/bin/node" # Copy bin items by hand, in case user has installed global npm modules into cache. cp -f "$dir/bin/node" "$N_PREFIX/bin" [[ -e "$dir/bin/node-waf" ]] && cp -f "$dir/bin/node-waf" "$N_PREFIX/bin" # v0.8.x if [[ -z "${N_PRESERVE_COREPACK}" ]]; then [[ -e "$dir/bin/corepack" ]] && cp -fR "$dir/bin/corepack" "$N_PREFIX/bin" # from 16.9.0 fi if [[ -z "${N_PRESERVE_NPM}" ]]; then [[ -e "$dir/bin/npm" ]] && cp -fR "$dir/bin/npm" "$N_PREFIX/bin" [[ -e "$dir/bin/npx" ]] && cp -fR "$dir/bin/npx" "$N_PREFIX/bin" fi # include mkdir -p "$N_PREFIX/include" find "$dir/include" -mindepth 1 -maxdepth 1 -exec cp -fR "{}" "$N_PREFIX/include" \; # share mkdir -p "$N_PREFIX/share" # Copy everything except man, at it is a symlink on some Linux (e.g. archlinux). find "$dir/share" -mindepth 1 -maxdepth 1 \! -name man -exec cp -fR "{}" "$N_PREFIX/share" \; mkdir -p "$N_PREFIX/share/man" find "$dir/share/man" -mindepth 1 -maxdepth 1 -exec cp -fR "{}" "$N_PREFIX/share/man" \; disable_pax_mprotect "${installed_node}" local active_node="$(command -v node)" if [[ -e "${active_node}" && -e "${installed_node}" && "${active_node}" != "${installed_node}" ]]; then # Installed and active are different which might be a PATH problem. List both to give user some clues. log "installed" "$("${installed_node}" --version) to ${installed_node}" log "active" "$("${active_node}" --version) at ${active_node}" else local npm_version_str="" local installed_npm="${N_PREFIX}/bin/npm" local active_npm="$(command -v npm)" if [[ -z "${N_PRESERVE_NPM}" && -e "${active_npm}" && -e "${installed_npm}" && "${active_npm}" = "${installed_npm}" ]]; then npm_version_str=" (with npm $(npm --version))" fi log "installed" "$("${installed_node}" --version)${npm_version_str}" # Extra tips for changed location. if [[ -e "${active_node}" && -e "${original_node}" && "${active_node}" != "${original_node}" ]]; then printf '\nNote: the node command changed location and the old location may be remembered in your current shell.\n' log old "${original_node}" log new "${active_node}" printf 'If "node --version" shows the old version then start a new shell, or reset the location hash with:\nhash -r (for bash, zsh, ash, dash, and ksh)\nrehash (for csh and tcsh)\n' fi fi if [[ "$CLEANUP" == "true" ]]; then log "cleanup" "removing cached $version" remove_versions "$version" fi } # # Install # install() { [[ -z "$1" ]] && abort "version required" local version get_latest_resolved_version "$1" || return 2 version="${g_target_node}" [[ -n "${version}" ]] || abort "no version found for '$1'" update_mirror_settings_for_version "$1" update_xz_settings_for_version "${version}" update_arch_settings_for_version "${version}" local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" # Note: decompression flags ignored with default Darwin tar which autodetects. if test "$N_USE_XZ" = "true"; then local tarflag="-Jx" else local tarflag="-zx" fi if test -d "$dir"; then if [[ ! -e "$dir/n.lock" ]] ; then if [[ "$DOWNLOAD" == "false" ]] ; then activate "${g_mirror_folder_name}/${version}" else log downloaded "${g_mirror_folder_name}/${version} already in cache" fi exit fi fi if [[ "$OFFLINE" == "true" ]]; then abort "version unavailable offline" fi if [[ "$DOWNLOAD" == "false" ]]; then log installing "${g_mirror_folder_name}-v$version" else log download "${g_mirror_folder_name}-v$version" fi local url="$(tarball_url "$version")" is_ok "${url}" || abort "download preflight failed for '$version' ($(display_masked_url "${url}"))" log mkdir "$dir" mkdir -p "$dir" || abort "sudo required (or change ownership, or define N_PREFIX)" touch "$dir/n.lock" cd "${dir}" || abort "Failed to cd to ${dir}" log fetch "$(display_masked_url "${url}")" do_get "${url}" | tar "$tarflag" --strip-components=1 --no-same-owner -f - pipe_results=( "${PIPESTATUS[@]}" ) if [[ "${pipe_results[0]}" -ne 0 ]]; then abort "failed to download archive for $version" fi if [[ "${pipe_results[1]}" -ne 0 ]]; then abort "failed to extract archive for $version" fi [ "$GET_SHOWS_PROGRESS" = "true" ] && erase_line rm -f "$dir/n.lock" disable_pax_mprotect bin/node if [[ "$DOWNLOAD" == "false" ]]; then activate "${g_mirror_folder_name}/$version" fi } # # Be more silent. # set_quiet() { SHOW_VERBOSE_LOG="false" command -v curl > /dev/null && CURL_OPTIONS+=( "--silent" ) && GET_SHOWS_PROGRESS="false" } # # Synopsis: do_get [option...] url # Call curl or wget with combination of global and passed options. # function do_get() { if command -v curl &> /dev/null; then curl "${CURL_OPTIONS[@]}" "$@" elif command -v wget &> /dev/null; then wget "${WGET_OPTIONS[@]}" "$@" else abort "curl or wget command required" fi } # # Synopsis: do_get_index [option...] url # Call curl or wget with combination of global and passed options, # with options tweaked to be more suitable for getting index. # function do_get_index() { if command -v curl &> /dev/null; then # --silent to suppress progress et al curl --silent "${CURL_OPTIONS[@]}" "$@" elif command -v wget &> /dev/null; then wget "${WGET_OPTIONS[@]}" "$@" else abort "curl or wget command required" fi } # # Synopsis: remove_versions version ... # function remove_versions() { [[ -z "$1" ]] && abort "version(s) required" while [[ $# -ne 0 ]]; do local version get_latest_resolved_version "$1" || break version="${g_target_node}" if [[ -n "${version}" ]]; then update_mirror_settings_for_version "$1" local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" if [[ -s "${dir}" ]]; then rm -rf "${dir}" else echo "$1 (${version}) not in downloads cache" fi else echo "No version found for '$1'" fi shift done } # # Synopsis: prune_cache # function prune_cache() { set_active_node for folder_and_version in $(display_versions_paths); do if [[ "${folder_and_version}" != "${g_active_node}" ]]; then echo "${folder_and_version}" rm -rf "${CACHE_DIR:?}/${folder_and_version}" fi done } # # Synopsis: find_cached_version version # Finds cache directory for resolved version. # Globals modified: # - g_cached_version function find_cached_version() { [[ -z "$1" ]] && abort "version required" local version get_latest_resolved_version "$1" || exit 1 version="${g_target_node}" [[ -n "${version}" ]] || abort "no version found for '$1'" update_mirror_settings_for_version "$1" g_cached_version="${CACHE_DIR}/${g_mirror_folder_name}/${version}" if [[ ! -d "${g_cached_version}" && "${DOWNLOAD}" == "true" ]]; then (install "${version}") fi [[ -d "${g_cached_version}" ]] || abort "'$1' (${version}) not in downloads cache" } # # Synopsis: display_bin_path_for_version version # function display_bin_path_for_version() { find_cached_version "$1" echo "${g_cached_version}/bin/node" } # # Synopsis: run_with_version version [args...] # Run the given of node with [args ..] # function run_with_version() { find_cached_version "$1" shift # remove version from parameters exec "${g_cached_version}/bin/node" "$@" } # # Synopsis: exec_with_version command [args...] # Modify the path to include and execute command. # function exec_with_version() { find_cached_version "$1" shift # remove version from parameters PATH="${g_cached_version}/bin:$PATH" exec "$@" } # # Synopsis: is_ok url # Check the HEAD response of . # function is_ok() { # Note: both curl and wget can follow redirects, as present on some mirrors (e.g. https://npm.taobao.org/mirrors/node). # The output is complicated with redirects, so keep it simple and use command status rather than parse output. if command -v curl &> /dev/null; then do_get --silent --head "$1" > /dev/null || return 1 else do_get --spider "$1" > /dev/null || return 1 fi } # # Synopsis: can_use_xz # Test system to see if xz decompression is supported by tar. # function can_use_xz() { # Be conservative and only enable if xz is likely to work. Unfortunately we can't directly query tar itself. # For research, see https://github.com/shadowspawn/nvh/issues/8 local uname_s="$(uname -s)" if [[ "${uname_s}" = "Linux" ]] && command -v xz &> /dev/null ; then # tar on linux is likely to support xz if it is available as a command return 0 elif [[ "${uname_s}" = "Darwin" ]]; then local macos_version="$(sw_vers -productVersion)" local macos_major_version="$(echo "${macos_version}" | cut -d '.' -f 1)" local macos_minor_version="$(echo "${macos_version}" | cut -d '.' -f 2)" if [[ "${macos_major_version}" -gt 10 || "${macos_minor_version}" -gt 8 ]]; then # tar on recent Darwin has xz support built-in return 0 fi fi return 2 # not supported } # # Synopsis: display_tarball_platform # function display_tarball_platform() { # https://en.wikipedia.org/wiki/Uname local os="unexpected_os" local uname_a="$(uname -a)" case "${uname_a}" in Linux*) os="linux" ;; Darwin*) os="darwin" ;; SunOS*) os="sunos" ;; AIX*) os="aix" ;; CYGWIN*) >&2 echo_red "Cygwin is not supported by n" ;; MINGW*) >&2 echo_red "Git BASH (MSYS) is not supported by n" ;; esac # architecture might already be known from (priority order): # * --arch flag on the command line, # * otherwise, from $N_ARCH # * otherwise from version specific adjustment (if applicable, see update_arch_settings_for_version) local arch="$ARCH" if [[ -z "$arch" ]]; then arch="unexpected_arch" local uname_m="$(uname -m)" case "${uname_m}" in x86_64) arch=x64 ;; i386 | i686) arch="x86" ;; aarch64) arch=arm64 ;; armv8l) arch=arm64 ;; # armv8l probably supports arm64, and there is no specific armv8l build so give it a go *) # e.g. armv6l, armv7l, arm64 arch="${uname_m}" ;; esac fi echo "${os}-${arch}" } # # Synopsis: display_compatible_file_field # display for current platform, as per field in index.tab, which is different than actual download # function display_compatible_file_field { local compatible_file_field="$(display_tarball_platform)" if [[ -z "${ARCH}" && "${compatible_file_field}" = "darwin-arm64" ]]; then # Look for arm64 for native but also x64 for older versions which can run in rosetta. # (Downside is will get an install error if install version above 16 with x64 and not arm64.) compatible_file_field="osx-arm64-tar|osx-x64-tar" elif [[ "${compatible_file_field}" =~ darwin-(.*) ]]; then compatible_file_field="osx-${BASH_REMATCH[1]}-tar" fi echo "${compatible_file_field}" } # # Synopsis: tarball_url version # function tarball_url() { local version="$1" local ext=gz [ "$N_USE_XZ" = "true" ] && ext="xz" echo "${g_mirror_url}/v${version}/node-v${version}-$(display_tarball_platform).tar.${ext}" } # # Synopsis: get_file_node_version filename # Sets g_target_node # function get_file_node_version() { g_target_node= local filepath="$1" verbose_log "found" "${filepath}" # read returns a non-zero status but does still work if there is no line ending local version <"${filepath}" read -r version # trim possible trailing \d from a Windows created file version="${version%%[[:space:]]}" verbose_log "read" "${version}" g_target_node="${version}" } # # Synopsis: get_package_engine_version\ # Sets g_target_node # function get_package_engine_version() { g_target_node= local filepath="$1" verbose_log "found" "${filepath}" local range if command -v jq &> /dev/null; then range="$(jq -r '.engines.node // ""' < "${filepath}")" elif command -v node &> /dev/null; then range="$(node -e "package = require('${filepath}'); if (package && package.engines && package.engines.node) console.log(package.engines.node)")" else abort "either jq or an active version of node is required to read 'engines' from package.json" fi verbose_log "read" "${range}" [[ -n "${range}" ]] || return 2 if [[ "*" == "${range}" ]]; then verbose_log "target" "current" g_target_node="current" return fi local version if [[ "${range}" =~ ^([>~^=]|\>\=)?v?([0-9]+(\.[0-9]+){0,2})(.[xX*])?$ ]]; then local operator="${BASH_REMATCH[1]}" version="${BASH_REMATCH[2]}" case "${operator}" in '' | =) ;; \> | \>=) version="current" ;; \~) [[ "${version}" =~ ^([0-9]+\.[0-9]+)\.[0-9]+$ ]] && version="${BASH_REMATCH[1]}" ;; ^) [[ "${version}" =~ ^([0-9]+) ]] && version="${BASH_REMATCH[1]}" ;; esac verbose_log "target" "${version}" else command -v npx &> /dev/null || abort "an active version of npx is required to use complex 'engine' ranges from package.json" [[ "$OFFLINE" != "true" ]] || abort "offline: an internet connection is required for looking up complex 'engine' ranges from package.json" verbose_log "resolving" "${range}" local version_per_line="$(n lsr --all)" local versions_one_line=$(echo "${version_per_line}" | tr '\n' ' ') # Using semver@7 so works with older versions of node. # shellcheck disable=SC2086 version=$(npm_config_yes=true npx --quiet semver@7 -r "${range}" ${versions_one_line} | tail -n 1) fi g_target_node="${version}" } # # Synopsis: get_nvmrc_version # Sets g_target_node # function get_nvmrc_version() { g_target_node= local filepath="$1" verbose_log "found" "${filepath}" local version <"${filepath}" read -r version # remove trailing comment, after # version="$(echo "${version}" | sed 's/[[:space:]]*#.*//')" verbose_log "read" "${version}" # Translate from nvm aliases case "${version}" in lts/\*) version="lts" ;; lts/*) version="${version:4}" ;; node) version="current" ;; *) ;; esac g_target_node="${version}" } # # Synopsis: get_engine_version [error-message] # Sets g_target_node # function get_engine_version() { g_target_node= local error_message="${1-package.json not found}" local parent parent="${PWD}" while [[ -n "${parent}" ]]; do if [[ -e "${parent}/package.json" ]]; then get_package_engine_version "${parent}/package.json" else parent=${parent%/*} continue fi break done [[ -n "${parent}" ]] || abort "${error_message}" [[ -n "${g_target_node}" ]] || abort "did not find supported version of node in 'engines' field of package.json" } # # Synopsis: get_auto_version # Sets g_target_node # function get_auto_version() { g_target_node= # Search for a version control file first local parent parent="${PWD}" while [[ -n "${parent}" ]]; do if [[ -e "${parent}/.n-node-version" ]]; then get_file_node_version "${parent}/.n-node-version" elif [[ -e "${parent}/.node-version" ]]; then get_file_node_version "${parent}/.node-version" elif [[ -e "${parent}/.nvmrc" ]]; then get_nvmrc_version "${parent}/.nvmrc" else parent=${parent%/*} continue fi break done # Fallback to package.json [[ -n "${parent}" ]] || get_engine_version "no file found for auto version (.n-node-version, .node-version, .nvmrc, or package.json)" [[ -n "${g_target_node}" ]] || abort "file found for auto did not contain target version of node" } # # Synopsis: get_latest_resolved_version version # Sets g_target_node # function get_latest_resolved_version() { g_target_node= local version=${1} # Transform some labels before processing further to allow fast-track for exact numeric versions. if [[ "${version}" = "auto" ]]; then get_auto_version || return 2 version="${g_target_node}" elif [[ "${version}" = "engine" ]]; then get_engine_version || return 2 version="${g_target_node}" fi simple_version=${version#node/} # Only place supporting node/ [sic] if is_exact_numeric_version "${simple_version}"; then # Just numbers, already resolved, no need to lookup first. simple_version="${simple_version#v}" g_target_node="${simple_version}" elif [[ "$OFFLINE" == "true" ]]; then g_target_node=$(display_local_versions "${version}") else # Complicated recognising exact version, KISS and lookup. g_target_node=$(N_MAX_REMOTE_MATCHES=1 display_remote_versions "$version") fi } # # Synopsis: display_remote_index # index.tab reference: https://github.com/nodejs/nodejs-dist-indexer # Index fields are: version date files npm v8 uv zlib openssl modules lts security # KISS and just return fields we currently care about: version files lts # display_remote_index() { local index_url="${g_mirror_url}/index.tab" # tail to remove header line do_get_index "${index_url}" | tail -n +2 | cut -f 1,3,10 if [[ "${PIPESTATUS[0]}" -ne 0 ]]; then # Reminder: abort will only exit subshell, but consistent error display abort "failed to download version index ($(display_masked_url "${index_url}"))" fi } # # Synopsis: display_match_limit limit # function display_match_limit(){ if [[ "$1" -gt 1 && "$1" -lt 32000 ]]; then echo "Listing remote... Displaying $1 matches (use --all to see all)." fi } # # Synopsis: display_local_versions version # function display_local_versions() { local version="$1" local match='.' verbose_log "offline" "matching cached versions" # Transform some labels before processing further. if is_node_support_version "${version}"; then version="$(display_latest_node_support_alias "${version}")" match_count=1 elif [[ "${version}" = "auto" ]]; then get_auto_version || return 2 version="${g_target_node}" elif [[ "${version}" = "engine" ]]; then get_engine_version || return 2 version="${g_target_node}" fi if [[ "${version}" = "latest" || "${version}" = "current" ]]; then match='^node/.' elif is_exact_numeric_version "${version}"; then # Quote any dots in version so they are literal for expression match="^node/${version//\./\.}" elif is_numeric_version "${version}"; then version="${version#v}" # Quote any dots in version so they are literal for expression match="${version//\./\.}" # Avoid 1.2 matching 1.23 match="^node/${match}[^0-9]" # elif is_lts_codename "${version}"; then # see if demand elif is_download_folder "${version}"; then match="^${version}/" # elif is_download_version "${version}"; then # see if demand else abort "invalid version '$1' for offline matching" fi display_versions_paths \ | n_grep -E "${match}" \ | tail -n 1 \ | sed 's|node/||' } # # Synopsis: display_remote_versions version # function display_remote_versions() { local version="$1" update_mirror_settings_for_version "${version}" local match='.' local match_count="${N_MAX_REMOTE_MATCHES}" # Transform some labels before processing further. if is_node_support_version "${version}"; then version="$(display_latest_node_support_alias "${version}")" match_count=1 elif [[ "${version}" = "auto" ]]; then get_auto_version || return 2 version="${g_target_node}" elif [[ "${version}" = "engine" ]]; then get_engine_version || return 2 version="${g_target_node}" fi if [[ -z "${version}" ]]; then match='.' elif [[ "${version}" = "lts" || "${version}" = "stable" ]]; then match_count=1 # Codename is last field, first one with a name is newest lts match="${TAB_CHAR}[a-zA-Z]+\$" elif [[ "${version}" = "latest" || "${version}" = "current" ]]; then match_count=1 match='.' elif is_numeric_version "${version}"; then version="v${version#v}" # Avoid restriction message if exact version is_exact_numeric_version "${version}" && match_count=1 # Quote any dots in version so they are literal for expression match="${version//\./\.}" # Avoid 1.2 matching 1.23 match="^${match}[^0-9]" elif is_lts_codename "${version}"; then # Capitalise (could alternatively make grep case insensitive) codename="$(echo "${version:0:1}" | tr '[:lower:]' '[:upper:]')${version:1}" # Codename is last field match="${TAB_CHAR}${codename}\$" elif is_download_folder "${version}"; then match='.' elif is_download_version "${version}"; then version="${version#"${g_mirror_folder_name}"/}" if [[ "${version}" = "latest" || "${version}" = "current" ]]; then match_count=1 match='.' else version="v${version#v}" match="${version//\./\.}" match="^${match}" # prefix if is_numeric_version "${version}"; then # Exact numeric match match="${match}[^0-9]" fi fi else abort "invalid version '$1'" fi display_match_limit "${match_count}" # Implementation notes: # - using awk rather than head so do not close pipe early on curl # - restrict search to compatible files as not always available, or not at same time # - return status of curl command (i.e. PIPESTATUS[0]) display_remote_index \ | n_grep -E "$(display_compatible_file_field)" \ | n_grep -E "${match}" \ | awk "NR<=${match_count}" \ | cut -f 1 \ | n_grep -E -o '[^v].*' return "${PIPESTATUS[0]}" } # # Synopsis: delete_with_echo target # function delete_with_echo() { if [[ -e "$1" ]]; then echo "$1" rm -rf "$1" fi } # # Synopsis: uninstall_installed # Uninstall the installed node and npm (leaving alone the cache), # so undo install, and may expose possible system installed versions. # uninstall_installed() { # npm: https://docs.npmjs.com/misc/removing-npm # rm -rf /usr/local/{lib/node{,/.npm,_modules},bin,share/man}/npm* # node: https://stackabuse.com/how-to-uninstall-node-js-from-mac-osx/ # Doing it by hand rather than scanning cache, so still works if cache deleted first. # This covers tarballs for at least node 4 through 10. while true; do read -r -p "Do you wish to delete node and npm from ${N_PREFIX}? " yn case $yn in [Yy]* ) break ;; [Nn]* ) exit ;; * ) echo "Please answer yes or no.";; esac done echo "" echo "Uninstalling node and npm" delete_with_echo "${N_PREFIX}/bin/node" delete_with_echo "${N_PREFIX}/bin/npm" delete_with_echo "${N_PREFIX}/bin/npx" delete_with_echo "${N_PREFIX}/bin/corepack" delete_with_echo "${N_PREFIX}/include/node" delete_with_echo "${N_PREFIX}/lib/dtrace/node.d" delete_with_echo "${N_PREFIX}/lib/node_modules/npm" delete_with_echo "${N_PREFIX}/lib/node_modules/corepack" delete_with_echo "${N_PREFIX}/share/doc/node" delete_with_echo "${N_PREFIX}/share/man/man1/node.1" delete_with_echo "${N_PREFIX}/share/systemtap/tapset/node.stp" } # # Synopsis: show_permission_suggestions # function show_permission_suggestions() { echo "Suggestions:" echo "- run n with sudo, or" if [[ "${N_CACHE_PREFIX}" == "${N_PREFIX}" ]]; then echo "- define N_PREFIX to a writeable location, or" else echo "- define N_PREFIX and N_CACHE_PREFIX to writeable locations, or" fi } # # Synopsis: show_diagnostics # Show environment and check for common problems. # function show_diagnostics() { echo "This information is to help you diagnose issues, and useful when reporting an issue." echo "Note: some output may contain passwords. Redact before sharing." printf "\n\nCOMMAND LOCATIONS AND VERSIONS\n" printf "\nbash\n" command -v bash && bash --version printf "\nn\n" command -v n && n --version printf "\nnode\n" if command -v node &> /dev/null; then node --version node -e 'if (process.versions.v8) console.log("JavaScript engine: v8");' printf "\nnpm\n" command -v npm && npm --version fi printf "\ntar\n" if command -v tar &> /dev/null; then tar --version else echo_red "tar not found. Needed for extracting downloads." fi printf "\ncurl or wget\n" if command -v curl &> /dev/null; then curl --version elif command -v wget &> /dev/null; then wget --version else echo_red "Neither curl nor wget found. Need one of them for downloads." fi printf "\njq\n" command -v jq && jq --version printf "\nuname\n" uname -a printf "\n\nSETTINGS\n" printf "\nn\n" echo "node mirror: $(display_masked_url "${N_NODE_MIRROR}")" echo "node downloads mirror: $(display_masked_url "${N_NODE_DOWNLOAD_MIRROR}")" echo "install destination: ${N_PREFIX}" [[ -n "${N_PREFIX}" ]] && echo "PATH: ${PATH}" [[ -n "$N_ARCH" ]] && echo "default arch: $N_ARCH" echo "ls-remote max matches: ${N_MAX_REMOTE_MATCHES}" [[ -n "${N_PRESERVE_NPM}" ]] && echo "installs preserve npm by default" [[ -n "${N_PRESERVE_COREPACK}" ]] && echo "installs preserve corepack by default" printf "\nProxy\n" # disable "var is referenced but not assigned": https://github.com/koalaman/shellcheck/wiki/SC2154 # shellcheck disable=SC2154 [[ -n "${http_proxy}" ]] && echo "http_proxy: ${http_proxy}" # shellcheck disable=SC2154 [[ -n "${https_proxy}" ]] && echo "https_proxy: ${https_proxy}" if command -v curl &> /dev/null; then # curl supports lower case and upper case! # shellcheck disable=SC2154 [[ -n "${all_proxy}" ]] && echo "all_proxy: ${all_proxy}" [[ -n "${ALL_PROXY}" ]] && echo "ALL_PROXY: ${ALL_PROXY}" [[ -n "${HTTP_PROXY}" ]] && echo "HTTP_PROXY: ${HTTP_PROXY}" [[ -n "${HTTPS_PROXY}" ]] && echo "HTTPS_PROXY: ${HTTPS_PROXY}" if [[ -e "${CURL_HOME}/.curlrc" ]]; then echo "have \$CURL_HOME/.curlrc" elif [[ -e "${HOME}/.curlrc" ]]; then echo "have \$HOME/.curlrc" fi elif command -v wget &> /dev/null; then if [[ -e "${WGETRC}" ]]; then echo "have \$WGETRC" elif [[ -e "${HOME}/.wgetrc" ]]; then echo "have \$HOME/.wgetrc" fi fi printf "\n\nCHECKS\n" printf "\nChecking n install destination is in PATH...\n" local install_bin="${N_PREFIX}/bin" local path_wth_guards=":${PATH}:" if [[ "${path_wth_guards}" =~ :${install_bin}/?: ]]; then printf "good\n" else echo_red "'${install_bin}' is not in PATH" fi if command -v node &> /dev/null; then printf "\nChecking n install destination priority in PATH...\n" local node_dir="$(dirname "$(command -v node)")" local index=0 local path_entry local path_entries local install_bin_index=0 local node_index=999 IFS=':' read -ra path_entries <<< "${PATH}" for path_entry in "${path_entries[@]}"; do (( index++ )) [[ "${path_entry}" =~ ^${node_dir}/?$ ]] && node_index="${index}" [[ "${path_entry}" =~ ^${install_bin}/?$ ]] && install_bin_index="${index}" done if [[ "${node_index}" -lt "${install_bin_index}" ]]; then echo_red "There is a version of node installed which will be found in PATH before the n installed version." else printf "good\n" fi fi # Check npm too. Simpler check than for PATH and node, more like the runtime logging for active/installed node. if [[ -z "${N_PRESERVE_NPM}" ]]; then printf "\nChecking npm install destination...\n" local installed_npm="${N_PREFIX}/bin/npm" local active_npm="$(command -v npm)" if [[ -e "${active_npm}" && -e "${installed_npm}" && "${active_npm}" != "${installed_npm}" ]]; then echo_red "There is an active version of npm shadowing the version installed by n. Check order of entries in PATH." log "installed" "${installed_npm}" log "active" "${active_npm}" else printf "good\n" fi fi printf "\nChecking prefix folders...\n" if [[ ! -e "${N_PREFIX}" ]]; then echo "Folder does not exist: ${N_PREFIX}" echo "- This folder will be created when you do an install." fi if [[ "${N_PREFIX}" != "${N_CACHE_PREFIX}" && ! -e "${N_CACHE_PREFIX}" ]]; then echo "Folder does not exist: ${N_CACHE_PREFIX}" echo "- This folder will be created when you do an install." fi if [[ -e "${N_PREFIX}" && -e "${N_CACHE_PREFIX}" ]]; then echo "good" fi if [[ -e "${N_CACHE_PREFIX}" ]]; then printf "\nChecking permissions for cache folder...\n" # Using knowledge cache path ends in /n/versions in following check. if [[ ! -e "${CACHE_DIR}" && (( -e "${N_CACHE_PREFIX}/n" && ! -w "${N_CACHE_PREFIX}/n" ) || ( ! -e "${N_CACHE_PREFIX}/n" && ! -w "${N_CACHE_PREFIX}" )) ]]; then echo_red "You do not have write permission to create: ${CACHE_DIR}" show_permission_suggestions echo "- make a folder you own:" echo " sudo mkdir -p \"${CACHE_DIR}\"" echo " sudo chown $(whoami) \"${CACHE_DIR}\"" elif [[ ! -e "${CACHE_DIR}" ]]; then echo "Cache folder does not exist: ${CACHE_DIR}" echo "- This is normal if you have not done an install yet, as cache is only created when needed." elif [[ ! -w "${CACHE_DIR}" ]]; then echo_red "You do not have write permission to: ${CACHE_DIR}" show_permission_suggestions echo "- change folder ownership to yourself:" echo " sudo chown -R $(whoami) \"${CACHE_DIR}\"" else echo "good" fi fi if [[ -e "${N_PREFIX}" ]]; then printf "\nChecking permissions for install folders...\n" local install_writeable="true" for subdir in bin lib include share; do if [[ -e "${N_PREFIX}/${subdir}" && ! -w "${N_PREFIX}/${subdir}" ]]; then install_writeable="false" echo_red "You do not have write permission to: ${N_PREFIX}/${subdir}" break fi if [[ ! -e "${N_PREFIX}/${subdir}" && ! -w "${N_PREFIX}" ]]; then install_writeable="false" echo_red "You do not have write permission to create: ${N_PREFIX}/${subdir}" break fi done if [[ "${install_writeable}" = "true" ]]; then echo "good" else show_permission_suggestions echo "- change folder ownerships to yourself:" echo " cd \"${N_PREFIX}\"" echo " sudo mkdir -p bin lib include share" echo " sudo chown -R $(whoami) bin lib include share" fi fi printf "\nChecking mirror is reachable...\n" if is_ok "${N_NODE_MIRROR}/"; then printf "good\n" else echo_red "mirror not reachable" printf "Showing failing command and output\n" if command -v curl &> /dev/null; then ( set -x; do_get --head "${N_NODE_MIRROR}/" ) else ( set -x; do_get --spider "${N_NODE_MIRROR}/" ) printf "\n" fi fi } # # Handle arguments. # # First pass. Process the options so they can come before or after commands, # particularly for `n lsr --all` and `n install --arch x686` # which feel pretty natural. unprocessed_args=() positional_arg="false" while [[ $# -ne 0 ]]; do case "$1" in --all) N_MAX_REMOTE_MATCHES=32000 ;; -V|--version) display_n_version ;; -h|--help|help) display_help; exit ;; -q|--quiet) set_quiet ;; -d|--download) DOWNLOAD="true" ;; --cleanup) CLEANUP="true" ;; --offline) OFFLINE="true" ;; --insecure) set_insecure ;; -p|--preserve) N_PRESERVE_NPM="true" N_PRESERVE_COREPACK="true" ;; --no-preserve) N_PRESERVE_NPM="" N_PRESERVE_COREPACK="" ;; --use-xz) N_USE_XZ="true" ;; --no-use-xz) N_USE_XZ="false" ;; --latest) display_remote_versions latest; exit ;; --stable) display_remote_versions lts; exit ;; # [sic] old terminology --lts) display_remote_versions lts; exit ;; -a|--arch) shift; set_arch "$1";; # set arch and continue exec|run|as|use) unprocessed_args+=( "$1" ) positional_arg="true" ;; *) if [[ "${positional_arg}" == "true" ]]; then unprocessed_args+=( "$@" ) break fi unprocessed_args+=( "$1" ) ;; esac shift done if [[ -z "${N_USE_XZ+defined}" ]]; then N_USE_XZ="true" # Default to using xz can_use_xz || N_USE_XZ="false" fi set -- "${unprocessed_args[@]}" if test $# -eq 0; then test -z "$(display_versions_paths)" && err_no_installed_print_help menu_select_cache_versions else case "$1" in bin|which) display_bin_path_for_version "$2"; exit ;; run|as|use) shift; run_with_version "$@"; exit ;; exec) shift; exec_with_version "$@"; exit ;; doctor) show_diagnostics; exit ;; rm|-) shift; remove_versions "$@"; exit ;; prune) prune_cache; exit ;; latest) install latest; exit ;; stable) install stable; exit ;; lts) install lts; exit ;; ls|list) display_versions_paths; exit ;; lsr|ls-remote|list-remote) shift; display_remote_versions "$1"; exit ;; uninstall) uninstall_installed; exit ;; i|install) shift; install "$1"; exit ;; download) shift; DOWNLOAD="true"; install "$1"; exit ;; N_TEST_DISPLAY_LATEST_RESOLVED_VERSION) shift; get_latest_resolved_version "$1" > /dev/null || exit 2; echo "${g_target_node}"; exit ;; *) install "$1"; exit ;; esac fi ================================================ FILE: docs/changing-node-location.md ================================================ # Switching To `n` Managed Node.js If you already have Node.js installed to a different root than `n` uses, you can easily end up with multiple copies of node (and npm, and npx, and globally installed packages!). Some common situations are you already had Node.js installed using your Linux package manager, or using another node version manager, or using say Homebrew. The two main ways you might resolve this are: - uninstall from the old directory and reinstall to the new directory - put the `bin` directory that `n` uses early in the `PATH` environment variable, so the `n` installed node is found first The simplest setup to understand is the first one. Just have one version of `node` installed. Let's walk-through the process of switching over from using Homebrew as an example. Let's start off with Node.js installed, `npm` updated, and an example global npm package. The key point is there are two install prefixes involved: - old: `/opt/homebrew` - new: `/usr/local` ```console % brew install node % npm install --global npm@latest % npm install --global @shadowspawn/forest-arborist % brew list node /opt/homebrew/Cellar/node/21.7.3/bin/node /opt/homebrew/Cellar/node/21.7.3/bin/npm /opt/homebrew/Cellar/node/21.7.3/bin/npx /opt/homebrew/Cellar/node/21.7.3/etc/bash_completion.d/npm /opt/homebrew/Cellar/node/21.7.3/include/node/ (107 files) /opt/homebrew/Cellar/node/21.7.3/libexec/bin/ (2 files) /opt/homebrew/Cellar/node/21.7.3/libexec/lib/ (2012 files) /opt/homebrew/Cellar/node/21.7.3/share/doc/ (2 files) /opt/homebrew/Cellar/node/21.7.3/share/man/man1/node.1 % command -v node /opt/homebrew/bin/node % command -v npm /opt/homebrew/bin/npm % npm prefix --global /opt/homebrew ``` Before we start transferring, list the global npm packages in the "old" location. We will refer back to this list. ```console % npm list --global /opt/homebrew/lib ├── @shadowspawn/forest-arborist@12.0.0 └── npm@10.5.0 ``` We could clean out the old location first, but let's install `n` and another copy of node and see what that looks like. We end up with two versions of node, and the active one is still the Homebrew managed version. ```console % brew install n % n lts installing : node-v20.12.2 mkdir : /usr/local/n/versions/node/20.12.2 fetch : https://nodejs.org/dist/v20.12.2/node-v20.12.2-darwin-arm64.tar.xz copying : node/20.12.2 installed : v20.12.2 to /usr/local/bin/node active : v21.7.3 at /opt/homebrew/bin/node % command -v node /opt/homebrew/bin/node % which -a node /opt/homebrew/bin/node /usr/local/bin/node % command -v npm /opt/homebrew/bin/npm % command -v npx /opt/homebrew/bin/npx % n doctor <...> CHECKS Checking n install destination is in PATH... good Checking n install destination priority in PATH... ⚠️ There is a version of node installed which will be found in PATH before the n installed version. Checking npm install destination... ⚠️ There is an active version of npm shadowing the version installed by n. Check order of entries in PATH. installed : /usr/local/bin/npm active : /opt/homebrew/bin/npm <...> ``` Now let's switch over. Delete everything from the old location. Delete all the global npm packages _except_ npm itself, then delete npm, then delete node. ```console npm uninstall --global @shadowspawn/forest-arborist npm uninstall --global npm brew uninstall node ``` Check the active binaries are now the ones installed by `n`: ```console % command -v node /usr/local/bin/node % command -v npm /usr/local/bin/npm % command -v npx /usr/local/bin/npx ``` And lastly, reinstall the global npm packages you started with: ``` % npm prefix --global /usr/local % npm install --global npm@latest % npm install --global @shadowspawn/forest-arborist % npm list -g /usr/local/lib ├── @shadowspawn/forest-arborist@12.0.0 └── npm@10.5.0 ``` ================================================ FILE: docs/proxy-server.md ================================================ # Proxy Server Under the hood, `n` uses `curl` or `wget` for the downloads. `curl` is used if available, and `wget` otherwise. Both `curl` and `wget` support using environment variables or startup files to set up the proxy. ## Using Environment Variable You can define the proxy server using an environment variable, which is read by multiple commands including `curl` and `wget`: export https_proxy='https://host:port/path' If your proxy requires authentication you can add the [url-encoded](https://urlencode.org) username and password into the URL. e.g. export https_proxy='https://encoded-user:encoded-password@host:port/path' If you have defined a custom node mirror which uses http, then you would define `http_proxy` rather than `https_proxy`. If you use `sudo` to run `n`, you need to do something extra to make the environment variables available. A simple way is to use `-E` (`--preserve-env`): sudo -E n lts ## Certificate Checks Your proxy server may supply its own ssl certificates for remote sites (as a man-in-the-middle). If you can not arrange to trust the proxy in this role, you can turn off (all) certificate checking with `--insecure`. e.g. n --insecure --lts Another possible work-around for certificate problems is to use plain http by specifying a custom node mirror: export NODE_MIRROR='http://nodejs.org/dist' export http_proxy='http://host:port/path' n --lts ## Startup Files An alternative to using an environment variable for the proxy settings is to configure a startup file for the command. Example `~/.curlrc` ([documentation](https://ec.haxx.se/cmdline-configfile.html)) proxy = http://host:port/path proxy-user = user:password # If need to disable certificate checks --insecure Example `~/.wgetrc` ([documentation](https://www.gnu.org/software/wget/manual/html_node/Wgetrc-Commands.html#Wgetrc-Commands)) https_proxy = http://host:port/path proxy_user = user proxy_password = password # If need to disable certificate checks check_certificate = off ## Troubleshooting To experiment and find what settings you need, use `curl` (or `wget`) directly with the node mirror and check the error messages. For these examples there is a proxy running on localhost:8080 which does not require authentication, but the certificates it offers are not trusted. First try fails because of the certificates and `curl` helpfully explains: $ curl --proxy localhost:8080 https://nodejs.org/dist/ curl: (60) SSL certificate problem: self signed certificate in certificate chain ... If you'd like to turn off curl's verification of the certificate, use the -k (or --insecure) option. HTTPS-proxy has similar options --proxy-cacert and --proxy-insecure. Once you get the command to work with settings appropriate for your setup, like: $ curl --insecure --proxy localhost:8080 https://nodejs.org/dist/ Index of /dist/ ... then you can try moving the proxy out of the command: $ https_proxy=localhost:8080 curl --insecure https://nodejs.org/dist/ Index of /dist/ ... and then `n` should work the same way: $ https_proxy=localhost:8080 n --insecure --lts 8.11.3 To make it permanent either add settings to the `curl` (or `wget`) startup file, or add the environment variable to your shell initialization file . e.g. export https_proxy=localhost:8080 For curl, two options of note for debugging are: -v, --verbose Makes curl verbose during the operation. Useful for debugging and seeing what's going on "under the hood". ... -q, --disable If used as the first parameter on the command line, the curlrc config file will not be read and used. ... ================================================ FILE: package.json ================================================ { "name": "n", "description": "Interactively Manage All Your Node Versions", "version": "10.2.0", "author": "TJ Holowaychuk ", "homepage": "https://github.com/tj/n", "bugs": "https://github.com/tj/n/issues", "contributors": [ { "name": "n Contributors", "url": "https://github.com/tj/n/graphs/contributors" } ], "keywords": [ "nvm", "node", "version", "manager", "switcher", "node", "binary", "env" ], "bin": { "n": "bin/n" }, "files": [ "bin/n" ], "repository": { "type": "git", "url": "git://github.com/tj/n.git" }, "scripts": { "test": "test/bin/run-all-tests" }, "devDependencies": { "bats": "^1.2.1", "bats-assert": "github:bats-core/bats-assert", "bats-support": "github:bats-core/bats-support" }, "preferGlobal": true, "os": [ "!win32" ], "engines": { "node": "*" }, "license": "MIT" } ================================================ FILE: test/bin/run-all-tests ================================================ #!/usr/bin/env bash BIN_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" # We want to cover curl and wget especially, gz and xz and variety of OS a bonus. services=( fedora-curl ubuntu-wget ) cd "$(dirname "${BIN_DIRECTORY}")" || exit 2 for service in "${services[@]}" ; do echo "${service}" docker compose run --rm "${service}" "bats" "/mnt/test/tests" echo "" done # host (current maintainer uses Mac) uname -s ../node_modules/.bin/bats tests ================================================ FILE: test/docker-base.yml ================================================ version: '2' # Define base service to specify the mounts and environment variables services: testbed: volumes: # make locally installed bats available in container (based on bats/install.sh) - ../node_modules/bats/bin/bats:/usr/local/bin/bats - ../node_modules/bats/libexec/bats-core:/usr/local/libexec/bats-core - ../node_modules/bats/lib/bats-core:/usr/local/lib/bats-core - ../node_modules/bats/man/bats.1:/usr/local/share/man/man1" - ../node_modules/bats/man/bats.7:/usr/local/share/man/man7" # the bats tests, same relative location to node_modules as in repo - ./tests:/mnt/test/tests # bats extra libraries, into similar relative location - ../node_modules/bats-support:/mnt/node_modules/bats-support - ../node_modules/bats-assert:/mnt/node_modules/bats-assert # the n script - ../bin/n:/usr/local/bin/n ================================================ FILE: test/docker-compose.yml ================================================ services: ubuntu-curl: extends: file: ./docker-base.yml service: testbed build: context: dockerfiles dockerfile: Dockerfile-ubuntu-curl ubuntu-wget: extends: file: ./docker-base.yml service: testbed build: context: dockerfiles dockerfile: Dockerfile-ubuntu-wget fedora-curl: extends: file: ./docker-base.yml service: testbed build: context: dockerfiles dockerfile: Dockerfile-fedora-curl ================================================ FILE: test/dockerfiles/Dockerfile-fedora-curl ================================================ FROM fedora:latest CMD ["/bin/bash"] ================================================ FILE: test/dockerfiles/Dockerfile-ubuntu-curl ================================================ FROM ubuntu:latest # curl RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* CMD ["/bin/bash"] ================================================ FILE: test/dockerfiles/Dockerfile-ubuntu-wget ================================================ FROM ubuntu:latest # wget RUN apt-get update \ && apt-get install -y wget \ && rm -rf /var/lib/apt/lists/* CMD ["/bin/bash"] ================================================ FILE: test/tests/install-contents.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' function setup() { unset_n_env } # Test that files get installed to expected locations # https://github.com/tj/n/issues/246 @test "install: contents" { readonly TARGET_VERSION="4.9.1" setup_tmp_prefix [ ! -d "${N_PREFIX}/n/versions" ] [ ! -d "${N_PREFIX}/bin" ] [ ! -d "${N_PREFIX}/include" ] [ ! -d "${N_PREFIX}/lib" ] [ ! -d "${N_PREFIX}/shared" ] n ${TARGET_VERSION} # Cached version [ -d "${N_PREFIX}/n/versions/node/${TARGET_VERSION}" ] # node and npm [ -f "${N_PREFIX}/bin/node" ] [ -f "${N_PREFIX}/bin/npm" ] # Installed something into each of other key folders [ -d "${N_PREFIX}/include/node" ] [ -d "${N_PREFIX}/lib/node_modules" ] [ -d "${N_PREFIX}/share/doc/node" ] # Did not install files from top level of tarball [ ! -f "${N_PREFIX}/README.md" ] output="$(node --version)" assert_equal "${output}" "v${TARGET_VERSION}" rm -rf "${TMP_PREFIX_DIR}" } @test "install: cache prefix" { readonly N_CACHE_PREFIX="$(mktemp -d)" readonly TARGET_VERSION="4.9.1" setup_tmp_prefix export N_CACHE_PREFIX [ ! -d "${N_CACHE_PREFIX}/n/versions/node/${TARGET_VERSION}" ] [ ! -d "${N_PREFIX}/n/versions/node/${TARGET_VERSION}" ] n ${TARGET_VERSION} # Cached version [ -d "${N_CACHE_PREFIX}/n/versions/node/${TARGET_VERSION}" ] [ ! -d "${N_PREFIX}/n/versions/node/${TARGET_VERSION}" ] rm -rf "${TMP_PREFIX_DIR}" "${N_CACHE_PREFIX}" } ================================================ FILE: test/tests/install-options.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' function setup() { unset_n_env setup_tmp_prefix } function teardown() { rm -rf "${TMP_PREFIX_DIR}" } @test "n --download 4.9.1" { # deprecated use of --download, replaced by download command n --download 4.9.1 [ -d "${N_PREFIX}/n/versions/node/4.9.1" ] [ ! -f "${N_PREFIX}/bin/node" ] [ ! -f "${N_PREFIX}/bin/npm" ] [ ! -d "${N_PREFIX}/include" ] [ ! -d "${N_PREFIX}/lib" ] [ ! -d "${N_PREFIX}/shared" ] } @test "n download 4.9.1" { # not an option, but keep with --download so stays in sync n download 4.9.1 [ -d "${N_PREFIX}/n/versions/node/4.9.1" ] [ ! -f "${N_PREFIX}/bin/node" ] [ ! -f "${N_PREFIX}/bin/npm" ] [ ! -d "${N_PREFIX}/include" ] [ ! -d "${N_PREFIX}/lib" ] [ ! -d "${N_PREFIX}/shared" ] } @test "n --quiet 4.9.1" { # just checking option is allowed, not testing functionality n --quiet 4.9.1 output="$(node --version)" assert_equal "${output}" "v4.9.1" } @test "n --cleanup 4.9.1" { n install --cleanup 4.9.1 output="$(node --version)" assert_equal "${output}" "v4.9.1" [ ! -d "${N_PREFIX}/n/versions/node/4.9.1" ] } # mostly --preserve, but also variations with i/install and lts/numeric @test "--preserve variations # (4 installs)" { local ARGON_VERSION="v4.9.1" local ARGON_NPM_VERSION="2.15.11" local LTS_VERSION="$(display_remote_version lts)" n ${ARGON_VERSION} output="$("${N_PREFIX}/bin/node" --version)" assert_equal "$output" "${ARGON_VERSION}" output="$("${N_PREFIX}/bin/npm" --version)" assert_equal "$output" "${ARGON_NPM_VERSION}" n --preserve "${LTS_VERSION}" output="$("${N_PREFIX}/bin/node" --version)" assert_equal "$output" "v${LTS_VERSION}" output="$("${N_PREFIX}/bin/npm" --version)" assert_equal "$output" "${ARGON_NPM_VERSION}" N_PRESERVE_NPM=1 n "${LTS_VERSION}" output="$("${N_PREFIX}/bin/npm" --version)" assert_equal "$output" "${ARGON_NPM_VERSION}" N_PRESERVE_NPM=1 n --no-preserve "${LTS_VERSION}" output="$("${N_PREFIX}/bin/npm" --version)" assert [ "$output" != "${ARGON_NPM_VERSION}" ] } # ToDo: --arch ================================================ FILE: test/tests/install-versions.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' function setup() { unset_n_env setup_tmp_prefix } function teardown() { rm -rf "${TMP_PREFIX_DIR}" } # Testing version permutations in lsr tests @test "n 4.9.1" { n 4.9.1 output="$(node --version)" assert_equal "${output}" "v4.9.1" } @test "n lts" { n lts output="$(node --version)" assert_equal "${output}" "v$(display_remote_version lts)" } @test "n latest" { n latest output="$(node --version)" assert_equal "${output}" "v$(display_remote_version latest)" } ================================================ FILE: test/tests/lookup.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' function setup() { unset_n_env } @test "n --lts" { output="$(n --lts)" local expected_version expected_version="$(display_remote_version lts)" expected_version="${expected_version#v}" assert_equal "${output}" "${expected_version}" } @test "n --stable" { output="$(n --stable)" local expected_version expected_version="$(display_remote_version lts)" expected_version="${expected_version#v}" assert_equal "${output}" "${expected_version}" } @test "n --latest" { output="$(n --latest)" local expected_version expected_version="$(display_remote_version latest)" expected_version="${expected_version#v}" assert_equal "${output}" "${expected_version}" } ================================================ FILE: test/tests/ls.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' function setup() { unset_n_env setup_tmp_prefix } function teardown() { rm -rf "${TMP_PREFIX_DIR}" } @test "n ls # just plain node" { # KISS and just make folders rather than do actual installs mkdir -p "${N_PREFIX}/n/versions/node/4.9.1" mkdir -p "${N_PREFIX}/n/versions/node/10.15.0" output="$(n ls)" assert_equal "${output}" "node/4.9.1 node/10.15.0" } @test "n list # mixed node and nightly" { local NIGHTLY_VERSION="12.0.0-nightly201812104aabd7ed64" # KISS and just make folders rather than do actual installs mkdir -p "${N_PREFIX}/n/versions/nightly/${NIGHTLY_VERSION}" mkdir -p "${N_PREFIX}/n/versions/node/10.15.0" output="$(n list)" assert_equal "${output}" "nightly/${NIGHTLY_VERSION} node/10.15.0" } ================================================ FILE: test/tests/lsr.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' function setup() { unset_n_env } # labels @test "n lsr lts" { output="$(n lsr lts)" assert_equal "${output}" "$(display_remote_version lts)" } @test "n lsr stable" { output="$(n lsr lts)" assert_equal "${output}" "$(display_remote_version lts)" } @test "n ls-remote latest" { output="$(n ls-remote latest)" assert_equal "${output}" "$(display_remote_version latest)" } @test "n list-remote current" { output="$(n list-remote current)" assert_equal "${output}" "$(display_remote_version latest)" } # codenames @test "n=1 n lsr argon" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr argon)" assert_equal "${output}" "4.9.1" } @test "n=1 n lsr Argon # case" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr Argon)" assert_equal "${output}" "4.9.1" } # numeric versions @test "n=1 n lsr 4" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr 4)" assert_equal "${output}" "4.9.1" } @test "n=1 n lsr v4" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr v4)" assert_equal "${output}" "4.9.1" } @test "n=1 n lsr 4.9" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr 4.9)" assert_equal "${output}" "4.9.1" } @test "n=1 n lsr 4.9.1" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr 4.9.1)" assert_equal "${output}" "4.9.1" } @test "n=1 n lsr v4.9.1" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr v4.9.1)" assert_equal "${output}" "4.9.1" } @test "n lsr 6.2 # multiple matches with header" { output="$(n lsr 6.2)" assert_equal "${output}" "Listing remote... Displaying 20 matches (use --all to see all). 6.2.2 6.2.1 6.2.0" } @test "n=1 n lsr --all 6.2 # --all, multiple matches with no header" { output="$(N_MAX_REMOTE_MATCHES=1 n --all lsr 6.2)" assert_equal "${output}" "6.2.2 6.2.1 6.2.0" } # Checking does not match 8.11 @test "n=1 n lsr v8.1 # numeric match" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr v8.1)" assert_equal "${output}" "8.1.4" } # Nightly @test "n=1 n lsr nightly" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr nightly)" assert_equal "${output}" "$(display_remote_version nightly)" } @test "n=1 n lsr nightly/" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr nightly/)" assert_equal "${output}" "$(display_remote_version nightly)" } @test "n=1 n lsr nightly/latest" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr nightly/latest)" assert_equal "${output}" "$(display_remote_version nightly)" } @test "n=1 n lsr nightly/v12.0.0-nightly2019040 # partial match" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr nightly/v12.0.0-nightly2019040)" assert_equal "${output}" "12.0.0-nightly2019040166b95362df" } # Numeric match should not find v7.10.1-nightly2017050369a8053e8a @test "n=1 n lsr nightly/12.0 # numeric match" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr nightly/12.0)" assert_equal "${output}" "12.0.0-nightly2019040166b95362df" } # Numeric match should not find v7.10.1-nightly2017050369a8053e8a @test "n=1 n lsr nightly/v12.0 # numeric match" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr nightly/v12.0)" assert_equal "${output}" "12.0.0-nightly2019040166b95362df" } @test "n lsr nightly/v12.0.0-nightly2019040166b95362df # exact" { output="$(N_MAX_REMOTE_MATCHES=1 n lsr nightly/v12.0.0-nightly2019040166b95362df)" assert_equal "${output}" "12.0.0-nightly2019040166b95362df" } ================================================ FILE: test/tests/offline.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' function setup_file() { unset_n_env setup_tmp_prefix # Note, NOT latest version of 16. n download 16.19.0 export N_NODE_MIRROR="https://no.internet.available" } function teardown_file() { rm -rf "${TMP_PREFIX_DIR}" } function setup() { hash -r } @test "n --offline 16" { n --offline 16 hash -r output="$(node --version)" assert_equal "${output}" "v16.19.0" rm "${TMP_PREFIX_DIR}/bin/node" } @test "n --offline latest" { n --offline latest hash -r output="$(node --version)" assert_equal "${output}" "v16.19.0" rm "${TMP_PREFIX_DIR}/bin/node" } @test "n --offline run 16..." { output="$(n --offline run 16 --version)" assert_equal "${output}" "v16.19.0" } @test "n --offline exec 16..." { output="$(n --offline exec 16 node --version)" assert_equal "${output}" "v16.19.0" } @test "n --offline which 16..." { output="$(n --offline which 16)" assert_equal "${output}" "${TMP_PREFIX_DIR}/n/versions/node/16.19.0/bin/node" } ================================================ FILE: test/tests/run-which.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' function setup_file() { unset_n_env # fixed directory so can reuse the two installs tmpdir="${TMPDIR:-/tmp}" export N_PREFIX="${tmpdir}/n/test/run-which" n download 4.9.1 n download lts # using "latest" for download tests with run and exec } function teardown_file() { rm -rf "${N_PREFIX}" } @test "setupAll for run/which/exec # (2 installs)" { # Dummy test so setupAll displayed while running first setup [ -d "${N_PREFIX}/n/versions/node/4.9.1" ] } # n which @test "n which 4" { output="$(n which 4)" assert_equal "$output" "${N_PREFIX}/n/versions/node/4.9.1/bin/node" } @test "n which v4.9.1" { output="$(n which v4.9.1)" assert_equal "$output" "${N_PREFIX}/n/versions/node/4.9.1/bin/node" } @test "n bin v4.9.1" { output="$(n bin v4.9.1)" assert_equal "$output" "${N_PREFIX}/n/versions/node/4.9.1/bin/node" } @test "n which argon" { output="$(n which argon)" assert_equal "$output" "${N_PREFIX}/n/versions/node/4.9.1/bin/node" } @test "n which lts" { output="$(n which lts)" local LTS_VERSION="$(display_remote_version lts)" assert_equal "$output" "${N_PREFIX}/n/versions/node/${LTS_VERSION}/bin/node" } # n run @test "n run 4" { output="$(n run 4 --version)" assert_equal "$output" "v4.9.1" } @test "n run lts" { output="$(n run lts --version)" local LTS_VERSION="$(display_remote_version lts)" assert_equal "$output" "v${LTS_VERSION}" } @test "n use 4" { output="$(n use 4 --version)" assert_equal "$output" "v4.9.1" } @test "n as 4" { output="$(n as 4 --version)" assert_equal "$output" "v4.9.1" } @test "n run --download latest" { n rm latest || true n run --download latest --version output="$(n run latest --version)" local LATEST_VERSION="$(display_remote_version latest)" assert_equal "$output" "v${LATEST_VERSION}" } # n exec @test "n exec v4.9.1 node" { output="$(n exec v4.9.1 node --version)" assert_equal "$output" "v4.9.1" } @test "n exec 4 npm" { output="$(n exec 4 npm --version)" assert_equal "$output" "2.15.11" } @test "n exec lts" { output="$(n exec lts node --version)" local LTS_VERSION="$(display_remote_version lts)" assert_equal "$output" "v${LTS_VERSION}" } @test "n exec -d latest" { n rm latest || true n exec -d latest node --version output="$(n exec latest node --version)" local LATEST_VERSION="$(display_remote_version latest)" assert_equal "$output" "v${LATEST_VERSION}" } ================================================ FILE: test/tests/shared-functions.bash ================================================ #!/usr/bin/env bash # unset the n environment variables so tests running from known state. # Globals: # lots function unset_n_env(){ unset N_PREFIX unset N_CACHE_PREFIX unset NODE_MIRROR unset N_NODE_MIRROR unset N_NODE_DOWNLOAD_MIRROR unset N_MAX_REMOTE_MATCHES unset N_PRESERVE_NPM unset N_PRESERVE_COREPACK unset HTTP_USER unset HTTP_PASSWORD unset GREP_OPTIONS } # Create temporary dir and configure n to use it. # Globals: # TMP_PREFIX_DIR # N_PREFIX # PATH function setup_tmp_prefix() { TMP_PREFIX_DIR="$(mktemp -d)" [ -d "${TMP_PREFIX_DIR}" ] || exit 2 # return a safer variable to `rm -rf` later than N_PREFIX export TMP_PREFIX_DIR export N_PREFIX="${TMP_PREFIX_DIR}" export PATH="${N_PREFIX}/bin:${PATH}" } # Display relevant file name (third field of index.tab) for current platform. # Based on code from nvm rather than n for independent approach. Simplified for just common platforms initially. # See list on https://github.com/nodejs/nodejs-dist-indexer function display_compatible_file_field() { local os="unexpected" case "$(uname -a)" in Linux\ *) os="linux" ;; Darwin\ *) os="osx" ;; esac local arch="unexpected" local uname_m uname_m="$(uname -m)" case "${uname_m}" in x86_64 | amd64) arch="x64" ;; i*86) arch="x86" ;; aarch64) arch="arm64" ;; *) arch="${uname_m}" ;; esac echo "${os}-${arch}" } # display_remote_version # Limited support for using index.tab to resolve version into a number. # Return version number, without leading v. # # The simper (and independent) code here can cause transient false positive failures, like if the latest nightly version # has not been build for all architectures yet. function display_remote_version() { # ToDo: support NODE_MIRROR local fetch if command -v curl &> /dev/null; then fetch="curl --silent --location --fail --compressed" else # insecure to match current n implementation fetch="wget -q -O- --no-check-certificate" fi local TAB_CHAR=$'\t' local match='xxx' local mirror="${N_NODE_MIRROR:-https://nodejs.org/dist}" if [[ "$1" = "lts" || "$1" = "stable" ]]; then match="^([^${TAB_CHAR}]+${TAB_CHAR}){9}[^-]" elif [[ "$1" = "latest" ]]; then match='.' elif [[ "$1" = "nightly" ]]; then match='.' mirror="${N_NODE_DOWNLOAD_MIRROR:-https://nodejs.org/download}/nightly" fi # Using awk rather than head so do not close pipe early on curl ${fetch} "${mirror}/index.tab" \ | tail -n +2 \ | grep "$(display_compatible_file_field)" \ | grep -E "${match}" \ | cut -f -1 \ | awk "NR==1" \ | grep -E -o '[^v].*' } ================================================ FILE: test/tests/uninstall.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' function setup() { unset_n_env setup_tmp_prefix } function teardown() { rm -rf "${TMP_PREFIX_DIR}" } @test "n uninstall (of lts)" { n lts [ -f "${N_PREFIX}/bin/node" ] [ -f "${N_PREFIX}/bin/npm" ] [ -f "${N_PREFIX}/lib/node_modules/npm/package.json" ] # Check we get all the files if we uninstall and rm cache. echo y | n uninstall n rm lts output="$(find "${N_PREFIX}" -not -type d)" assert_equal "$output" "" } @test "n uninstall (of nightly/latest)" { n nightly/latest [ -f "${N_PREFIX}/bin/node" ] [ -f "${N_PREFIX}/bin/npm" ] [ -f "${N_PREFIX}/lib/node_modules/npm/package.json" ] # Check we get all the files if we uninstall and rm cache. echo y | n uninstall n rm nightly/latest output="$(find "${N_PREFIX}" -not -type d)" assert_equal "$output" "" } ================================================ FILE: test/tests/version-auto-priority.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' # auto function setup_file() { unset_n_env tmpdir="${TMPDIR:-/tmp}" export MY_DIR="${tmpdir}/n/test/version-resolve-auto-priority" mkdir -p "${MY_DIR}" # Need a version of node available for reading package.json export N_PREFIX="${MY_DIR}" export PATH="${MY_DIR}/bin:${PATH}" n install lts } function teardown_file() { rm -rf "${MY_DIR}" } function setup() { # Bit fragile, but reuse directory and clean up between tests. rm -f "${MY_DIR}/package.json" rm -f "${MY_DIR}/.n-node-version" rm -f "${MY_DIR}/.node-version" rm -f "${MY_DIR}/.nvmrc" } @test ".n-node-version first" { cd "${MY_DIR}" echo "8.1.4" > .n-node-version echo "8.2.0" > .node-version echo "8.3.0" > .nvmrc echo '{ "engines" : { "node" : "v8.4.0" } }' > package.json output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.1.4" } @test ".node-version second" { cd "${MY_DIR}" echo "8.2.0" > .node-version echo "8.3.0" > .nvmrc echo '{ "engines" : { "node" : "v8.4.0" } }' > package.json output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.2.0" } @test ".nvmrc third" { cd "${MY_DIR}" echo "8.3.0" > .nvmrc echo '{ "engines" : { "node" : "v8.4.0" } }' > package.json output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.3.0" } @test ".package.json last" { cd "${MY_DIR}" echo '{ "engines" : { "node" : "v8.4.0" } }' > package.json output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.4.0" } @test ".package.json last, after parent scanning" { cd "${MY_DIR}" echo "8.2.0" > .node-version mkdir package cd package echo '{ "engines" : { "node" : "v8.4.0" } }' > package.json output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.2.0" rm package.json cd .. rmdir package } ================================================ FILE: test/tests/version-resolve-auto-engine.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' # auto # engine is a label too! These tests mostly use auto as first available only through auto. function setup_file() { unset_n_env tmpdir="${TMPDIR:-/tmp}" export MY_DIR="${tmpdir}/n/test/version-resolve-auto-engine" mkdir -p "${MY_DIR}" # Need a version of node and npx available for reading package.json export N_PREFIX="${MY_DIR}" export PATH="${MY_DIR}/bin:${PATH}" n install lts } function teardown_file() { rm -rf "${MY_DIR}" } function setup() { rm -f "${MY_DIR}/package.json" } function write_engine() { echo '{ "engines" : { "node" : "'"$1"'" } }' > package.json } @test "setupAll for auto-engine # (1 install)" { # Dummy test so setupAll displayed while running first setup } @test "auto engine, 8.9.0" { cd "${MY_DIR}" write_engine "8.9.0" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.9.0" } @test "auto engine, v8.9.1" { cd "${MY_DIR}" write_engine "v8.9.1" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.9.1" } @test "auto engine, =8.9.2" { cd "${MY_DIR}" write_engine "=8.9.2" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.9.2" } @test "auto engine, =v8.9.3" { cd "${MY_DIR}" write_engine "=v8.9.3" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.9.3" } @test "engine, =v8.9.4" { cd "${MY_DIR}" write_engine "=v8.9.4" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION engine)" assert_equal "${output}" "8.9.4" } @test "auto engine, >1" { local TARGET_VERSION="$(display_remote_version latest)" cd "${MY_DIR}" write_engine ">1" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "${TARGET_VERSION}" } @test "auto engine, >=2" { local TARGET_VERSION="$(display_remote_version latest)" cd "${MY_DIR}" write_engine ">=2" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "${TARGET_VERSION}" } @test "auto engine, 8" { cd "${MY_DIR}" write_engine "8" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.17.0" } @test "auto engine, 8.x" { cd "${MY_DIR}" write_engine "8.x" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.17.0" } @test "auto engine, 8.X" { cd "${MY_DIR}" write_engine "8.X" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.17.0" } @test "auto engine, 8.*" { cd "${MY_DIR}" write_engine "8.*" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.17.0" } @test "auto engine, ~8.11.0" { cd "${MY_DIR}" write_engine "~8.11.0" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.11.4" } @test "auto engine, ~8.11" { cd "${MY_DIR}" write_engine "~8.11" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.11.4" } @test "auto engine, ~8" { cd "${MY_DIR}" write_engine "~8" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.17.0" } @test "auto engine, ^8.11.0" { cd "${MY_DIR}" write_engine "^8.11.0" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.17.0" } @test "auto engine, ^8.x" { cd "${MY_DIR}" write_engine "^8.x" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.17.0" } @test "auto engine, subdir" { cd "${MY_DIR}" write_engine "8.11.2" mkdir -p sub-engine cd sub-engine output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.11.2" } @test "auto engine (semver), <8.12" { cd "${MY_DIR}" write_engine "<8.12" # newer versions of npx not liking proxy as used in tests output="$(https_proxy= n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.11.4" } @test "auto engine (semver), 8.11.1 - 8.11.3" { cd "${MY_DIR}" write_engine "8.11.1 - 8.11.3" # newer versions of npx not liking proxy as used in tests output="$(https_proxy= n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.11.3" } @test "auto engine (semver), >8.1 <8.12 || >2.1 <3.4" { cd "${MY_DIR}" write_engine ">8.1 <8.12 || >2.1 <3.4" # newer versions of npx not liking proxy as used in tests output="$(https_proxy= n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.11.4" } ================================================ FILE: test/tests/version-resolve-auto-file.bats ================================================ #!/usr/bin/env bats # Not testing all the permutations on both files, as know they are currenly implemented using same code! load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' # auto function setup_file() { unset_n_env tmpdir="${TMPDIR:-/tmp}" export MY_DIR="${tmpdir}/n/test/version-resolve-auto-file" mkdir -p "${MY_DIR}" } function teardown_file() { rm -rf "${MY_DIR}" } function setup() { rm -f "${MY_DIR}/.n-node-version" rm -f "${MY_DIR}/.node-version" } @test "auto, missing file" { cd "${MY_DIR}" run n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto assert [ "$status" -ne 0 ] } @test "auto .n-node-version, no eol" { cd "${MY_DIR}" printf "8.1.0" > .n-node-version output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.1.0" } @test "auto .n-node-version, unix eol" { cd "${MY_DIR}" printf "8.1.1\n" > .n-node-version output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.1.1" } @test "auto .n-node-version, Windows eol" { cd "${MY_DIR}" printf "8.1.2\r\n" > .n-node-version output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.1.2" } @test "auto .n-node-version, leading v" { cd "${MY_DIR}" printf "v8.1.3\n" > .n-node-version output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.1.3" } @test "auto .n-node-version, first line only" { cd "${MY_DIR}" printf "8.1.4\nmore text\n" > .n-node-version output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.1.4" } @test "auto .n-node-version, from sub directory" { cd "${MY_DIR}" printf "8.2.0\n" > .n-node-version mkdir -p sub6 cd sub6 output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.2.0" } @test "auto .node-version, partial version lookup" { # Check normal resolving cd "${MY_DIR}" printf "4.9\n" > .node-version output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "4.9.1" } @test "auto .node-version, from sub directory" { cd "${MY_DIR}" printf "8.2.1\n" > .n-node-version mkdir -p sub7 cd sub7 output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.2.1" } ================================================ FILE: test/tests/version-resolve-auto-nvmrc.bats ================================================ #!/usr/bin/env bats load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' # auto function setup_file() { unset_n_env tmpdir="${TMPDIR:-/tmp}" export MY_DIR="${tmpdir}/n/test/version-resolve-auto-nvmrc" mkdir -p "${MY_DIR}" } function teardown_file() { rm -rf "${MY_DIR}" } function setup() { rm -f "${MY_DIR}/.nvmrc" } @test "auto .nvmrc, numeric" { cd "${MY_DIR}" printf "8.10.0\n" > .nvmrc output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.10.0" } @test "auto .nvmrc, numeric with leading v" { cd "${MY_DIR}" printf "v8.11.0\n" > .nvmrc output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.11.0" } @test "auto .nvmrc, node" { local TARGET_VERSION="$(display_remote_version latest)" cd "${MY_DIR}" printf "node\n" > .nvmrc output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "${TARGET_VERSION}" } @test "auto .nvmrc, lts/*" { local TARGET_VERSION="$(display_remote_version lts)" cd "${MY_DIR}" printf "lts/*\n" > .nvmrc output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "${TARGET_VERSION}" } @test "auto .nvmrc, lts/argon" { local TARGET_VERSION="$(display_remote_version lts)" cd "${MY_DIR}" printf "lts/argon\n" > .nvmrc output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "4.9.1" } @test "auto .nvmrc, sub directory" { cd "${MY_DIR}" printf "v8.11.1\n" > .nvmrc mkdir -p sub-npmrc cd sub-npmrc output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "8.11.1" } @test "auto .nvmrc, trailing comment" { local TARGET_VERSION="8.10.0" cd "${MY_DIR}" printf "${TARGET_VERSION} # comment" > .nvmrc output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION auto)" assert_equal "${output}" "${TARGET_VERSION}" } ================================================ FILE: test/tests/version-resolve.bats ================================================ #!/usr/bin/env bats # Note: full semver is resolved without lookup, so can use arbitrary versions for testing like 999.999.999 load shared-functions load '../../node_modules/bats-support/load' load '../../node_modules/bats-assert/load' function setup() { unset_n_env } # node support aliases @test "display_latest_resolved_version active" { local TARGET_VERSION="$(display_remote_version latest)" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION active)" assert_equal "$output" "${TARGET_VERSION}" } @test "display_latest_resolved_version lts_active" { local TARGET_VERSION="$(display_remote_version lts)" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION lts_active)" assert_equal "$output" "${TARGET_VERSION}" } @test "display_latest_resolved_version lts_latest" { local TARGET_VERSION="$(display_remote_version lts)" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION lts_latest)" assert_equal "$output" "${TARGET_VERSION}" } @test "display_latest_resolved_version lts" { local TARGET_VERSION="$(display_remote_version lts)" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION lts)" assert_equal "$output" "${TARGET_VERSION}" } @test "display_latest_resolved_version current" { local TARGET_VERSION="$(display_remote_version latest)" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION current)" assert_equal "$output" "${TARGET_VERSION}" } @test "display_latest_resolved_version supported" { local TARGET_VERSION="$(display_remote_version latest)" output="$(n N_TEST_DISPLAY_LATEST_RESOLVED_VERSION supported)" assert_equal "$output" "${TARGET_VERSION}" } ================================================ FILE: test/tests.md ================================================ # Tests Automated tests for `n`. ## Setup Optional proxy using mitmproxy: # using homebrew (Mac) to install mitmproxy brew install mitmproxy ## Running Tests Run all the tests across a range of containers and on the host system: npm run test Run all the tests on a single system: cd test npx bats tests docker compose run ubuntu-curl bats /mnt/test/tests Run single test on a single system:: cd test npx bats tests/install-contents.bats docker compose run ubuntu-curl bats /mnt/test/tests/install-contents.bats ## Docker Tips Using `docker compose` in addition to `docker` for convenient mounting of `n` script and the tests into the container. Changes to the tests or to `n` itself are reflected immediately without needing to rebuild the containers. `bats` is being mounted directly out of `node_modules` into the container as a manual install based on its own install script. This is a bit of a hack, but avoids needing to install `git` or `npm` for a full remote install of `bats`, and means everything on the same version of `bats`. The containers each have: * either curl or wget (or both) installed Using `docker compose` to run the container adds: * specified `n` script mounted to `/usr/local/bin/n` * `test/tests` mounted to `/mnt/test/tests` * `node_modules/bats` provides `/usr/local/bin/bats` et al * `.curlrc` with `--insecure` to allow use of proxy So for example: cd test docker compose run ubuntu-curl # in container n --version bats /mnt/test/tests