rn - Rust Node manager ===================== [![build status](https://gitlab.com/bff/rn/badges/master/build.svg)](https://gitlab.com/bff/rn/commits/master) [![coverage report](https://gitlab.com/bff/rn/badges/master/coverage.svg)](https://gitlab.com/bff/rn/commits/master) > Nursing your node environment to good health! `rn` is yet another version manager for Node.js, a bit like [rvm](https://rvm.io/) is a Ruby version manager. It's primary goals are ease of use and fast execution for use in a local development environment on MacOS or Linux. ## Installation RN includes a [bash script](./install.sh) to install itself on MacOS and Linux and perform simple diagnostics on your shell: ```sh curl -sSL https://gitlab.com/bff/rn/raw/master/install.sh | bash ``` Check that it works (either platform): ```sh rn ``` ## Compiling from Source ```sh curl https://sh.rustup.rs -sSf | sh git clone git@gitlab.com:bff/rn.git cd rn cargo build --release sudo cp target/release/rn /usr/local/bin/rn cp bin/rn.sh .config/rn/ echo 'source $HOME/.config/rn/rn.sh' >> .bashrc ``` ## Usage ### Set node version in shell ```json { "engines": { "node": "0.10.37" } } ``` 1. Fully specify your node version in package.json using **an exact triple** as shown above. 2. CD into the directory with the package.json 3. There is no step 3. Node will just work as that version in your shell until CD into another node project. ### List versions ```sh $ rn --list 0.10.37 4.3.0 6.4.9 ``` ## Configuration ### envvar `RN_DIR` Defaults to `$HOME/.rn`; this is where `rn` will store node versions, and potentially metadata in the future. Node binaries are stored in `$RN_DIR/versions`. For example, if you had node 5.7.1 locally, and `RN_DIR` was unset, you could find the local copy of node 5.7.1 at `/home/bryce/.rn/versions/v5.7.1`. ## Features Comparison I think the easiest way to understand `rn` is to compare it to the other Node.js version managers in the space. Project | rn | nvm | nodenv[^1] | n ------------------------|-----------|-----------|------------|------------ Design Goal | local dev | local dev | prod & dev | system node Feature Set | tiny | large | enormous | small Implemented in | Rust | Bash/Zsh | All Shells | Bash Full Semver Support | never | yes | yes | yes Per-shell Versions | yes | yes | yes | no gLobal Version | soon | yes | yes | yes Sourced? | partially | yes | partially | no Package.json Parsing | yes | no | no | no Manages `npm` | soon | no | no | no Hooks `cd` | yes | no | no | no ## Design Decisions ### Why Rust? * I'm passionate about Rust * Even more than C or Go, if it compiles it almost definitely JustWorks (tm) * As a compiled language, Rust is cheap to into memory on every invocation making it faster for shell init than a large Bash script. * Although Bash is great for small script and single file utilities, it's not necessarily an ideal way to structure a large project that you want to maintain * Rust (and other compiled languages) are less prone to side-effecty problems prone to crop up in shell scripts * Rust offers great intuition into memory management * Rust has a fantastic for package management (compared to Go or Bash, for example) * Rust has excellent FFI support for calling into C code (making it less portable) which I've leverage quite a bit through dependencies * Rust offers opportunities to explore parallelizing network requests The biggest downsides to working with Rust are that it isn't as portable as Go or Bash. However, I think there are enough good technical reasons to justify using Rust inspite of the portability. ### Why Not Full Semver? My main goal was make life better for developers in their local shell. If the package.json specifies only the major version of Node.js, then it requires an additional network and more parsing to find acceptable verion to download and install. Additionally, I've worked on teams that found breaking changes happening on minor versions (semver not followed) or serious bugs emerge from changing even the patch version. I also happen to believe that having a high fidelity between one's development environment and production environment is a great way to avoid nasty surprises. By locking in the _exact_ patch version, I hope I'm saving you from some serious issues. Lastly, if those (hopefully) sound technical reasons don't sway you, I'll admit that I didn't want to go implement a full semver logic and I wanted to execute more quickly on this project. For all these reasons, I have no plans to implement full semver in the package.json parsing. ### Why Per-shell Versions of Node.js? It's often handy to run multiple Node.js applications simultaneous (for microservices, etc). By configuring each shell's PATH separately, I can ensure that each terminal session has exactly the version of node it needs. Think of it as a poor man's Docker. ### Why Is Sourcing a Large Bash File Bad? On 2015 MacBook Pro's, I've seen sourcing thousands of lines of Bash add seconds to the init time required to open a new shell. Imagine if every time you opened a new tab in Firefox, you had wait 2+ seconds for the location bar to become responsive. You'd switch to Chrome in a heartbeat. Sourcing large Bash files is a great way to avoid re-loading them every time you want to use their functionality. However, by leveraging Rust (or any compiled language for that matter), it's very cheap to load machine code into memory for each execution. This also allows me to skip the expensive upfront time spent sourcing a large Bash file. ### Why Override `cd`? Because rvm does it?! One problem with rvm's implementation is that it has a large feature set mostly built in Bash that's easy to screw up. By wrapping `cd` in a small, fast, and narrowed scoped Bash function, I can easily have the native Rust code do all the logic and heavy lifting of downloading node versions and updating the PATH with less opprotunity for things to go badly. However, to udpate your current shell and not have my changes to the shell disappear in a subshell, Bash functions are an obvious choice. Honestly, I can't think of another way to do it that's less terrible. If you know of one, open an issue and tell me! [^1]: I haven't use nodenv personally, so don't 100% trust my description of nodenv, gleaned from the documentation. ## How It Works `rn.sh` replaces the builtin `cd` utility with a Bash function. This function checks for the existence of a package.json file. If a package.json file is found, `rn` is invoked with the full path to that file. Internally, `rn` finds the previously downloaded versions of node (if any), and determines whether or not to download a tarball of the desired version of Node.js from nodejs.org. `rn` streams the response data through a decompression algorithm in memory and unpacks the decompressed archive into RN_DIR. Finally, whether or not a download occurred, `rn` will attempt to strip out any previously set node versions from the path and add the new node version to the front of the path. It prints out the final desired PATH over STDOUT and exits 0. `rn.sh` receives STDOUT and replaces PATH in the current shell. You're ready to run your project! If anything goes wrong, `rn` will print out an error to STDERR and exit 1; the PATH will not be updated. Helpful error messages have already been implemented for invalid package.json files and unusable node versions, among other scenarios. Additionally contextual error messages will be implemented as time permits.