Browse Source

Merge remote-tracking branch 'origin/master' into add-with-shims

remotes/origin/add-with-shims
Gal Schlezinger 2 years ago
parent
commit
db7a38c8de
  1. 8
      .changeset/README.md
  2. 5
      .changeset/chilly-apes-travel.md
  3. 14
      .changeset/config.json
  4. 5
      .changeset/red-flowers-collect.md
  5. 5
      .changeset/soft-laws-doubt.md
  6. 13
      .ci/generate-changelog.sh
  7. 94
      .ci/prepare-version.js
  8. 93
      .ci/print-command-docs.js
  9. 14
      .ci/record_screen.sh
  10. 11
      .ci/recorded_screen_script.sh
  11. 13
      .ci/type-letters.js
  12. 62
      .github/workflows/debug.yml
  13. 6
      .github/workflows/installation_script.yml
  14. 61
      .github/workflows/release.yml
  15. 210
      .github/workflows/rust.yml
  16. 2
      .gitignore
  17. 1
      .kodiak.toml
  18. 2
      .node-version
  19. 102
      CHANGELOG.md
  20. 1393
      Cargo.lock
  21. 45
      Cargo.toml
  22. 34
      README.md
  23. 802
      docs/commands.md
  24. 2
      docs/fnm.svg
  25. 269
      e2e/__snapshots__/aliases.test.ts.snap
  26. 284
      e2e/__snapshots__/basic.test.ts.snap
  27. 96
      e2e/__snapshots__/current.test.ts.snap
  28. 18
      e2e/__snapshots__/env.test.ts.snap
  29. 70
      e2e/__snapshots__/exec.test.ts.snap
  30. 28
      e2e/__snapshots__/existing-installation.test.ts.snap
  31. 32
      e2e/__snapshots__/latest-lts.test.ts.snap
  32. 127
      e2e/__snapshots__/log-level.test.ts.snap
  33. 67
      e2e/__snapshots__/multishell.test.ts.snap
  34. 32
      e2e/__snapshots__/nvmrc-lts.test.ts.snap
  35. 59
      e2e/__snapshots__/shims.test.ts.snap
  36. 46
      e2e/__snapshots__/uninstall.test.ts.snap
  37. 129
      e2e/aliases.test.ts
  38. 93
      e2e/basic.test.ts
  39. 47
      e2e/current.test.ts
  40. 15
      e2e/describe.ts
  41. 35
      e2e/env.test.ts
  42. 42
      e2e/exec.test.ts
  43. 22
      e2e/existing-installation.test.ts
  44. 19
      e2e/latest-lts.test.ts
  45. 71
      e2e/log-level.test.ts
  46. 27
      e2e/multishell.test.ts
  47. 24
      e2e/nvmrc-lts.test.ts
  48. 3
      e2e/shellcode/get-stderr.ts
  49. 179
      e2e/shellcode/script.ts
  50. 97
      e2e/shellcode/shells.ts
  51. 12
      e2e/shellcode/shells/cmdCall.ts
  52. 33
      e2e/shellcode/shells/cmdEnv.ts
  53. 52
      e2e/shellcode/shells/expect-command-output.ts
  54. 24
      e2e/shellcode/shells/output-contains.ts
  55. 16
      e2e/shellcode/shells/redirect-output.ts
  56. 20
      e2e/shellcode/shells/sub-shell.ts
  57. 15
      e2e/shellcode/shells/types.ts
  58. 9
      e2e/shellcode/test-bin-dir.ts
  59. 9
      e2e/shellcode/test-cwd.ts
  60. 10
      e2e/shellcode/test-node-version.ts
  61. 15
      e2e/shellcode/test-tmp-dir.ts
  62. 49
      e2e/shims.test.ts
  63. 38
      e2e/system-node.test.ts
  64. 29
      e2e/uninstall.test.ts
  65. 22
      jest.config.cjs
  66. 33
      package.json
  67. 5129
      pnpm-lock.yaml
  68. 28
      renovate.json
  69. 5
      site/package.json
  70. 10
      site/tsconfig.json
  71. 14
      site/vercel.json
  72. 677
      site/yarn.lock
  73. 26
      src/arch.rs
  74. 4
      src/archive/zip.rs
  75. 24
      src/choose_version_for_user_input.rs
  76. 38
      src/cli.rs
  77. 22
      src/commands/alias.rs
  78. 21
      src/commands/completions.rs
  79. 3
      src/commands/current.rs
  80. 3
      src/commands/default.rs
  81. 124
      src/commands/env.rs
  82. 72
      src/commands/exec.rs
  83. 70
      src/commands/install.rs
  84. 20
      src/commands/ls_local.rs
  85. 15
      src/commands/ls_remote.rs
  86. 16
      src/commands/unalias.rs
  87. 72
      src/commands/uninstall.rs
  88. 140
      src/commands/use.rs
  89. 83
      src/config.rs
  90. 18
      src/current_version.rs
  91. 9
      src/default_version.rs
  92. 26
      src/directories.rs
  93. 67
      src/downloader.rs
  94. 28
      src/installed_versions.rs
  95. 14
      src/log_level.rs
  96. 4
      src/main.rs
  97. 45
      src/shell/bash.rs
  98. 39
      src/shell/fish.rs
  99. 45
      src/shell/infer/mod.rs
  100. 121
      src/shell/infer/unix.rs
  101. Some files were not shown because too many files have changed in this diff Show More

8
.changeset/README.md

@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

5
.changeset/chilly-apes-travel.md

@ -0,0 +1,5 @@
---
"fnm": minor
---
Add `--json` to `fnm env` to output the env vars as JSON

14
.changeset/config.json

@ -0,0 +1,14 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
"changelog": [
"@svitejs/changesets-changelog-github-compact",
{ "repo": "Schniz/fnm" }
],
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": []
}

5
.changeset/red-flowers-collect.md

@ -0,0 +1,5 @@
---
"fnm": patch
---
This updates the Changesets configurations.

5
.changeset/soft-laws-doubt.md

@ -0,0 +1,5 @@
---
"fnm": patch
---
fix test: Use correct PATH for npm install test

13
.ci/generate-changelog.sh

@ -1,13 +0,0 @@
#!/bin/bash
if [ "$1" = "" ]; then
echo "No version provided, using 'Unreleased'" >&2
NEXT_VERSION="Unreleased"
else
NEXT_VERSION="$1"
fi
echo "Generating changelog for $NEXT_VERSION"
lerna-changelog --from=v1.0.0 "--next-version=$NEXT_VERSION" >CHANGELOG.md
prettier --write CHANGELOG.md

94
.ci/prepare-version.js

@ -2,85 +2,71 @@
/// @ts-check /// @ts-check
const fs = require("fs"); import fs from "fs"
const cp = require("child_process"); import cp from "child_process"
const path = require("path"); import cmd from "cmd-ts"
const cmd = require("cmd-ts"); import toml from "toml"
const toml = require("toml"); import assert from "assert"
const CARGO_TOML_PATH = path.join(__dirname, "../Cargo.toml"); const CARGO_TOML_PATH = new URL("../Cargo.toml", import.meta.url).pathname
const command = cmd.command({ const command = cmd.command({
name: "prepare-version", name: "prepare-version",
description: "Prepare a new fnm version", description: "Prepare a new fnm version",
args: { args: {},
versionType: cmd.positional({ async handler({}) {
displayName: "version type", updateCargoToml(await getPackageVersion())
type: cmd.oneOf(["patch", "minor", "major"]), exec("cargo build --release")
}), exec("yarn generate-command-docs --binary-path=./target/release/fnm")
exec("./.ci/record_screen.sh")
}, },
async handler({ versionType }) { })
exec("git pull --ff-only");
const nextVersion = updateCargoToml(versionType);
exec("cargo build --release");
exec("yarn generate-command-docs --binary-path=./target/release/fnm");
exec("./docs/record_screen.sh");
exec(`yarn changelog ${nextVersion}`);
},
});
cmd.run(cmd.binary(command), process.argv); cmd.run(cmd.binary(command), process.argv)
////////////////////// //////////////////////
// Helper functions // // Helper functions //
////////////////////// //////////////////////
function updateCargoToml(versionType) { /**
const cargoToml = fs.readFileSync(CARGO_TOML_PATH, "utf8"); * @returns {Promise<string>}
const cargoTomlContents = toml.parse(cargoToml); */
const currentVersion = cargoTomlContents.package.version; async function getPackageVersion() {
const nextVersion = changeVersion( const pkgJson = await fs.promises.readFile(
versionType, new URL("../package.json", import.meta.url),
cargoTomlContents.package.version "utf8"
); )
const version = JSON.parse(pkgJson).version
assert(version, "package.json version is not set")
return version
}
function updateCargoToml(nextVersion) {
const cargoToml = fs.readFileSync(CARGO_TOML_PATH, "utf8")
const cargoTomlContents = toml.parse(cargoToml)
const currentVersion = cargoTomlContents.package.version
const newToml = cargoToml.replace( const newToml = cargoToml.replace(
`version = "${currentVersion}"`, `version = "${currentVersion}"`,
`version = "${nextVersion}"` `version = "${nextVersion}"`
); )
if (newToml === cargoToml) { if (newToml === cargoToml) {
console.error("Cargo.toml didn't change, error!"); console.error("Cargo.toml didn't change, error!")
process.exitCode = 1; process.exitCode = 1
return; return
} }
fs.writeFileSync(CARGO_TOML_PATH, newToml, "utf8"); fs.writeFileSync(CARGO_TOML_PATH, newToml, "utf8")
return nextVersion; return nextVersion
} }
function exec(command, env) { function exec(command, env) {
console.log(`$ ${command}`); console.log(`$ ${command}`)
return cp.execSync(command, { return cp.execSync(command, {
cwd: path.join(__dirname, ".."), // root of repo cwd: new URL("..", import.meta.url),
stdio: "inherit", stdio: "inherit",
env: { ...process.env, ...env }, env: { ...process.env, ...env },
}); })
}
/**
* @param {"patch" | "minor" | "major"} type
* @param {string} version
*/
function changeVersion(type, version) {
const [major, minor, patch] = version.split(".").map((x) => parseInt(x, 10));
switch (type) {
case "patch":
return [major, minor, patch + 1].join(".");
case "minor":
return [major, minor + 1, 0].join(".");
case "major":
return [major + 1, 0, 0].join(".");
}
} }

93
.ci/print-command-docs.js

@ -2,24 +2,23 @@
/// @ts-check /// @ts-check
const execa = require("execa"); import { execa } from "execa"
const path = require("path"); import fs from "node:fs"
const fs = require("fs"); import cmd from "cmd-ts"
const cmd = require("cmd-ts"); import cmdFs from "cmd-ts/dist/cjs/batteries/fs.js"
const cmdFs = require("cmd-ts/dist/cjs/batteries/fs");
const FnmBinaryPath = { const FnmBinaryPath = {
...cmdFs.ExistingPath, ...cmdFs.ExistingPath,
defaultValue() { defaultValue() {
const target = path.join(__dirname, "../target/debug/fnm"); const target = new URL("../target/debug/fnm", import.meta.url)
if (!fs.existsSync(target)) { if (!fs.existsSync(target)) {
throw new Error( throw new Error(
"Can't find debug target, please run `cargo build` or provide a specific binary path" "Can't find debug target, please run `cargo build` or provide a specific binary path"
); )
} }
return target; return target.pathname
}, },
}; }
const command = cmd.command({ const command = cmd.command({
name: "print-command-docs", name: "print-command-docs",
@ -36,27 +35,27 @@ const command = cmd.command({
}), }),
}, },
async handler({ checkForDirty, fnmPath }) { async handler({ checkForDirty, fnmPath }) {
const targetFile = path.join(__dirname, "../docs/commands.md"); const targetFile = new URL("../docs/commands.md", import.meta.url).pathname
await main(targetFile, fnmPath); await main(targetFile, fnmPath)
if (checkForDirty) { if (checkForDirty) {
const gitStatus = await checkGitStatus(targetFile); const gitStatus = await checkGitStatus(targetFile)
if (gitStatus.state === "dirty") { if (gitStatus.state === "dirty") {
process.exitCode = 1; process.exitCode = 1
console.error( console.error(
"The file has changed. Please re-run `yarn generate-command-docs`." "The file has changed. Please re-run `yarn generate-command-docs`."
); )
console.error(`hint: The following diff was found:`); console.error(`hint: The following diff was found:`)
console.error(); console.error()
console.error(gitStatus.diff); console.error(gitStatus.diff)
} }
} }
}, },
}); })
cmd.run(cmd.binary(command), process.argv).catch((err) => { cmd.run(cmd.binary(command), process.argv).catch((err) => {
console.error(err); console.error(err)
process.exitCode = process.exitCode || 1; process.exitCode = process.exitCode || 1
}); })
/** /**
* @param {string} targetFile * @param {string} targetFile
@ -64,20 +63,20 @@ cmd.run(cmd.binary(command), process.argv).catch((err) => {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function main(targetFile, fnmPath) { async function main(targetFile, fnmPath) {
const stream = fs.createWriteStream(targetFile); const stream = fs.createWriteStream(targetFile)
const { subcommands, text: mainText } = await getCommandHelp(fnmPath); const { subcommands, text: mainText } = await getCommandHelp(fnmPath)
await write(stream, line(`fnm`, mainText)); await write(stream, line(`fnm`, mainText))
for (const subcommand of subcommands) { for (const subcommand of subcommands) {
const { text: subcommandText } = await getCommandHelp(fnmPath, subcommand); const { text: subcommandText } = await getCommandHelp(fnmPath, subcommand)
await write(stream, "\n" + line(`fnm ${subcommand}`, subcommandText)); await write(stream, "\n" + line(`fnm ${subcommand}`, subcommandText))
} }
stream.close(); stream.close()
await execa(`yarn`, ["prettier", "--write", targetFile]); await execa(`yarn`, ["prettier", "--write", targetFile])
} }
/** /**
@ -87,14 +86,14 @@ async function main(targetFile, fnmPath) {
*/ */
function write(stream, content) { function write(stream, content) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
stream.write(content, (err) => (err ? reject(err) : resolve())); stream.write(content, (err) => (err ? reject(err) : resolve()))
}); })
} }
function line(cmd, text) { function line(cmd, text) {
const cmdCode = "`" + cmd + "`"; const cmdCode = "`" + cmd + "`"
const textCode = "```\n" + text + "\n```"; const textCode = "```\n" + text + "\n```"
return `# ${cmdCode}\n${textCode}`; return `# ${cmdCode}\n${textCode}`
} }
/** /**
@ -103,23 +102,25 @@ function line(cmd, text) {
* @returns {Promise<{ subcommands: string[], text: string }>} * @returns {Promise<{ subcommands: string[], text: string }>}
*/ */
async function getCommandHelp(fnmPath, command) { async function getCommandHelp(fnmPath, command) {
const cmdArg = command ? [command] : []; const cmdArg = command ? [command] : []
const result = await run(fnmPath, [...cmdArg, "--help"]); const result = await run(fnmPath, [...cmdArg, "--help"])
const text = result.stdout; const text = result.stdout
const rows = text.split("\n"); const rows = text.split("\n")
const headerIndex = rows.findIndex((x) => x.includes("SUBCOMMANDS")); const headerIndex = rows.findIndex((x) => x.includes("SUBCOMMANDS"))
/** @type {string[]} */ /** @type {string[]} */
const subcommands = []; const subcommands = []
if (!command) { if (!command) {
for (const row of rows.slice(headerIndex + 1)) { for (const row of rows.slice(headerIndex + 1)) {
const words = row.split(/\s+/); const [, word] = row.split(/\s+/)
subcommands.push(words[1]); if (word && word[0].toLowerCase() === word[0]) {
subcommands.push(word)
}
} }
} }
return { return {
subcommands, subcommands,
text, text,
}; }
} }
/** /**
@ -131,7 +132,7 @@ function run(fnmPath, args) {
reject: false, reject: false,
stdout: "pipe", stdout: "pipe",
stderr: "pipe", stderr: "pipe",
}); })
} }
/** /**
@ -145,9 +146,9 @@ async function checkGitStatus(targetFile) {
{ {
reject: false, reject: false,
} }
); )
if (exitCode === 0) { if (exitCode === 0) {
return { state: "clean" }; return { state: "clean" }
} }
return { state: "dirty", diff: stdout }; return { state: "dirty", diff: stdout }
} }

14
docs/record_screen.sh → .ci/record_screen.sh

@ -3,10 +3,15 @@
DIRECTORY="$(dirname "$0")" DIRECTORY="$(dirname "$0")"
function setup_binary() { function setup_binary() {
TEMP_DIR="$(mktemp -d -t fnm)" TEMP_DIR="/tmp/fnm-$(date '+%s')"
mkdir "$TEMP_DIR"
cp ./target/release/fnm "$TEMP_DIR/fnm" cp ./target/release/fnm "$TEMP_DIR/fnm"
export PATH=$TEMP_DIR:$PATH export PATH=$TEMP_DIR:$PATH
export FNM_DIR=$TEMP_DIR/.fnm export FNM_DIR=$TEMP_DIR/.fnm
# First run of the binary might be slower due to anti-virus software
echo "Using $(which fnm)"
echo " with version $(fnm --version)"
} }
setup_binary setup_binary
@ -16,4 +21,9 @@ RECORDING_PATH=$DIRECTORY/screen_recording
(rm -rf "$RECORDING_PATH" &> /dev/null || true) (rm -rf "$RECORDING_PATH" &> /dev/null || true)
asciinema rec -c "$DIRECTORY/recorded_screen_script.sh" "$RECORDING_PATH" asciinema rec -c "$DIRECTORY/recorded_screen_script.sh" "$RECORDING_PATH"
sed "s@$TEMP_DIR@~@g" "$RECORDING_PATH" | svg-term --window --out "$DIRECTORY/fnm.svg" --height=17 --width=70 sed "s@$TEMP_DIR@~@g" "$RECORDING_PATH" | \
svg-term \
--window \
--out "docs/fnm.svg" \
--height=17 \
--width=70

11
docs/recorded_screen_script.sh → .ci/recorded_screen_script.sh

@ -1,16 +1,19 @@
#!/bin/zsh #!/bin/bash
set -e set -e
GAL_PROMPT_PREFIX='\e[34m✡ \e[0m' export PATH=$PATH_ADDITION:$PATH
GAL_PROMPT_PREFIX="\e[34m✡\e[m "
function type() { function type() {
printf $GAL_PROMPT_PREFIX printf $GAL_PROMPT_PREFIX
echo $* | pv -qL $[10+(-2 + RANDOM%5)] echo -n " "
echo $* | node .ci/type-letters.js
} }
type 'eval "$(fnm env)"' type 'eval "$(fnm env)"'
eval `fnm env` eval "$(fnm env)"
type 'fnm --version' type 'fnm --version'
fnm --version fnm --version

13
.ci/type-letters.js

@ -0,0 +1,13 @@
(async () => {
for await (const chunk of process.stdin) {
const letters = chunk.toString("utf8").split("");
for (const letter of letters) {
process.stdout.write(letter);
await sleep(Math.random() * 100 + 20);
}
}
})();
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

62
.github/workflows/debug.yml

@ -0,0 +1,62 @@
name: "debug"
on:
workflow_dispatch:
concurrency:
group: debug
cancel-in-progress: true
jobs:
e2e_windows_debug:
runs-on: windows-latest
name: "e2e/windows/debug"
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.commit_hash }}
- name: Download artifact
id: download-artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: rust.yml
workflow_conclusion: ""
branch: ${{ env.GITHUB_REF }}
name: "fnm-windows"
path: "target/release"
if_no_artifact_found: ignore
- uses: hecrj/setup-rust-action@v1
if: steps.download-artifact.outputs.artifact-found == false
with:
rust-version: stable
- uses: Swatinem/rust-cache@v2
if: steps.download-artifact.outputs.artifact-found == false
- name: Build release binary
if: steps.download-artifact.outputs.artifact-found == false
run: cargo build --release
env:
RUSTFLAGS: "-C target-feature=+crt-static"
- uses: pnpm/action-setup@v2.2.4
with:
run_install: false
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- run: pnpm install
- name: 🐛 Debug Build
if: always()
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true

6
.github/workflows/installation_script.yml

@ -21,8 +21,8 @@ jobs:
steps: steps:
- name: Set up QEMU - name: Set up QEMU
id: qemu id: qemu
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Run installation script in Docker - name: Run installation script in Docker
run: | run: |
docker run --rm -v $(pwd):$(pwd) -e "RUST_LOG=fnm=debug" --workdir $(pwd) ${{matrix.docker_image}} bash -c ' docker run --rm -v $(pwd):$(pwd) -e "RUST_LOG=fnm=debug" --workdir $(pwd) ${{matrix.docker_image}} bash -c '
@ -62,7 +62,7 @@ jobs:
script_arguments: '--force-no-brew' script_arguments: '--force-no-brew'
runs-on: ${{ matrix.setup.os }}-latest runs-on: ${{ matrix.setup.os }}-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- run: "sudo apt-get install -y ${{ matrix.shell }}" - run: "sudo apt-get install -y ${{ matrix.shell }}"
name: Install ${{matrix.shell}} using apt-get name: Install ${{matrix.shell}} using apt-get
if: matrix.setup.os == 'ubuntu' if: matrix.setup.os == 'ubuntu'

61
.github/workflows/release.yml

@ -0,0 +1,61 @@
name: release
on:
push:
branches:
- master
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
create_pull_request:
runs-on: ubuntu-latest
steps:
# set up
- uses: actions/checkout@v3
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
- uses: Swatinem/rust-cache@v2
- uses: pnpm/action-setup@v2.2.4
with:
run_install: false
# pnpm
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install script dependencies
run: |
sudo apt-get update
sudo apt-get install -y asciinema
- name: Install Node.js project dependencies
run: pnpm install
- name: Create Release Pull Request
uses: changesets/action@v1
with:
version: "pnpm version:prepare"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TERM: xterm

210
.github/workflows/rust.yml

@ -6,6 +6,10 @@ on:
branches: branches:
- master - master
concurrency:
group: ci-${{ github.head_ref }}
cancel-in-progress: true
jobs: jobs:
fmt: fmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -13,7 +17,8 @@ jobs:
- uses: hecrj/setup-rust-action@v1 - uses: hecrj/setup-rust-action@v1
with: with:
rust-version: stable rust-version: stable
- uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- name: cargo fmt - name: cargo fmt
run: cargo fmt -- --check run: cargo fmt -- --check
@ -23,7 +28,8 @@ jobs:
- uses: hecrj/setup-rust-action@v1 - uses: hecrj/setup-rust-action@v1
with: with:
rust-version: stable rust-version: stable
- uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- name: cargo clippy - name: cargo clippy
run: cargo clippy -- -D warnings run: cargo clippy -- -D warnings
@ -36,28 +42,10 @@ jobs:
- uses: hecrj/setup-rust-action@v1 - uses: hecrj/setup-rust-action@v1
with: with:
rust-version: stable rust-version: stable
- uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v2
- name: Run tests - uses: actions/checkout@v3
run: cargo test -- --skip=feature_tests
e2e_tests:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- name: Install Fish and Zsh using brew
if: "startsWith(matrix.os, 'macOS')"
run: brew install fish zsh
- name: Install Fish and Zsh using apt
if: "startsWith(matrix.os, 'ubuntu')"
run: sudo apt-get install -y fish zsh
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
- uses: actions/checkout@v2
- name: Run tests - name: Run tests
run: cargo test -- feature_tests run: cargo test
build_release: build_release:
runs-on: windows-latest runs-on: windows-latest
@ -66,12 +54,13 @@ jobs:
- uses: hecrj/setup-rust-action@v1 - uses: hecrj/setup-rust-action@v1
with: with:
rust-version: stable rust-version: stable
- uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- name: Build release binary - name: Build release binary
run: cargo build --release run: cargo build --release
env: env:
RUSTFLAGS: "-C target-feature=+crt-static" RUSTFLAGS: "-C target-feature=+crt-static"
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v3
with: with:
name: fnm-windows name: fnm-windows
path: target/release/fnm.exe path: target/release/fnm.exe
@ -83,7 +72,8 @@ jobs:
- uses: hecrj/setup-rust-action@v1 - uses: hecrj/setup-rust-action@v1
with: with:
rust-version: stable rust-version: stable
- uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- name: Build release binary - name: Build release binary
run: cargo build --release run: cargo build --release
env: env:
@ -92,11 +82,155 @@ jobs:
run: strip target/release/fnm run: strip target/release/fnm
- name: List dynamically linked libraries - name: List dynamically linked libraries
run: otool -L target/release/fnm run: otool -L target/release/fnm
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v3
with: with:
name: fnm-macos name: fnm-macos
path: target/release/fnm path: target/release/fnm
e2e_macos:
runs-on: macos-latest
needs: [build_macos_release]
name: "e2e/macos"
steps:
- name: install necessary shells
run: brew install fish zsh bash
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: fnm-macos
path: target/release
- name: mark binary as executable
run: chmod +x target/release/fnm
- uses: pnpm/action-setup@v2.2.4
with:
run_install: false
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- run: pnpm install
- run: pnpm test
env:
FNM_TARGET_NAME: "release"
FORCE_COLOR: "1"
e2e_windows:
runs-on: windows-latest
needs: [build_release]
name: "e2e/windows"
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: fnm-windows
path: target/release
- uses: pnpm/action-setup@v2.2.4
with:
run_install: false
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- run: pnpm install
- run: pnpm test
env:
FNM_TARGET_NAME: "release"
FORCE_COLOR: "1"
# e2e_windows_debug:
# runs-on: windows-latest
# name: "e2e/windows/debug"
# environment: Debug
# needs: [e2e_windows]
# if: contains(join(needs.*.result, ','), 'failure')
# steps:
# - uses: actions/checkout@v3
# - uses: actions/download-artifact@v3
# with:
# name: fnm-windows
# path: target/release
# - uses: pnpm/action-setup@v2.2.2
# with:
# run_install: false
# - uses: actions/setup-node@v3
# with:
# node-version: 16.x
# cache: 'pnpm'
# - name: Get pnpm store directory
# id: pnpm-cache
# run: |
# echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
# - uses: actions/cache@v3
# name: Setup pnpm cache
# with:
# path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
# key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
# restore-keys: |
# ${{ runner.os }}-pnpm-store-
# - run: pnpm install
# - name: 🐛 Debug Build
# uses: mxschmitt/action-tmate@v3
e2e_linux:
runs-on: ubuntu-latest
needs: [build_static_linux_binary]
name: "e2e/linux"
steps:
- name: install necessary shells
run: sudo apt-get update && sudo apt-get install -y fish zsh bash
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: fnm-linux
path: target/release
- name: mark binary as executable
run: chmod +x target/release/fnm
- uses: pnpm/action-setup@v2.2.4
with:
run_install: false
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- run: pnpm install
- run: pnpm test
env:
FNM_TARGET_NAME: "release"
FORCE_COLOR: "1"
build_static_linux_binary: build_static_linux_binary:
name: "Build static Linux binary" name: "Build static Linux binary"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -105,16 +239,19 @@ jobs:
with: with:
rust-version: stable rust-version: stable
targets: x86_64-unknown-linux-musl targets: x86_64-unknown-linux-musl
- uses: Swatinem/rust-cache@v2
with:
key: static-linux-binary
- name: Install musl tools - name: Install musl tools
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y --no-install-recommends musl-tools sudo apt-get install -y --no-install-recommends musl-tools
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Build release binary - name: Build release binary
run: cargo build --release --target x86_64-unknown-linux-musl run: cargo build --release --target x86_64-unknown-linux-musl
- name: Strip binary from debug symbols - name: Strip binary from debug symbols
run: strip target/x86_64-unknown-linux-musl/release/fnm run: strip target/x86_64-unknown-linux-musl/release/fnm
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v3
with: with:
name: fnm-linux name: fnm-linux
path: target/x86_64-unknown-linux-musl/release/fnm path: target/x86_64-unknown-linux-musl/release/fnm
@ -138,20 +275,23 @@ jobs:
steps: steps:
- name: Set up QEMU - name: Set up QEMU
id: qemu id: qemu
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- uses: hecrj/setup-rust-action@v1 - uses: hecrj/setup-rust-action@v1
with: with:
rust-version: stable rust-version: stable
- uses: Swatinem/rust-cache@v2
with:
key: arm-binary-${{ matrix.arch }}
- name: 'Download `cross` crate' - name: 'Download `cross` crate'
run: cargo install cross run: cargo install cross
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: "Build release" - name: "Build release"
run: cross build --target $RUST_TARGET --release run: cross build --target $RUST_TARGET --release
- name: Compress binary using UPX - name: Compress binary using UPX
run: | run: |
sudo apt-get install -y upx sudo apt-get install -y upx
upx target/$RUST_TARGET/release/fnm upx target/$RUST_TARGET/release/fnm
- uses: uraimo/run-on-arch-action@v2.1.1 - uses: uraimo/run-on-arch-action@v2.1.2
name: Sanity test name: Sanity test
with: with:
arch: ${{matrix.docker_platform}} arch: ${{matrix.docker_platform}}
@ -176,7 +316,7 @@ jobs:
echo "fnm exec --using=12 -- node --version" echo "fnm exec --using=12 -- node --version"
/artifacts/fnm exec --using=12 -- node --version /artifacts/fnm exec --using=12 -- node --version
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v3
with: with:
name: fnm-${{ matrix.arch }} name: fnm-${{ matrix.arch }}
path: target/${{ env.RUST_TARGET }}/release/fnm path: target/${{ env.RUST_TARGET }}/release/fnm
@ -186,9 +326,9 @@ jobs:
name: Ensure command docs are up-to-date name: Ensure command docs are up-to-date
needs: [build_static_linux_binary] needs: [build_static_linux_binary]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Download a single artifact - name: Download a single artifact
uses: actions/download-artifact@v2 uses: actions/download-artifact@v3
with: with:
name: fnm-linux name: fnm-linux
- name: Make the binary runnable - name: Make the binary runnable

2
.gitignore vendored

@ -1,5 +1,5 @@
node_modules/ node_modules/
docs/screen_recording .ci/screen_recording
/benchmarks/results /benchmarks/results
/target /target
**/*.rs.bk **/*.rs.bk

1
.kodiak.toml

@ -0,0 +1 @@
version = 1

2
.node-version

@ -1 +1 @@
16.13.1 16.18.1

102
CHANGELOG.md

@ -1,4 +1,102 @@
## 1.28.2 (2021-12-05) ## 1.31.0 (2022-02-16)
## 1.31.1
### Patch Changes
- 6e6bdd8: Add changesets
#### Bugfix 🐛
- [#671](https://github.com/Schniz/fnm/pull/671) fix native-creds errors (using the unpublished version of reqwest) ([@Schniz](https://github.com/Schniz))
- [#663](https://github.com/Schniz/fnm/pull/663) remove .unwrap() and .expect() in shell code ([@Schniz](https://github.com/Schniz))
- [#656](https://github.com/Schniz/fnm/pull/656) Remove unwrap in fnm env, make error more readable ([@Schniz](https://github.com/Schniz))
#### Committers: 1
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
## v1.30.1 (2022-01-30)
#### Bugfix 🐛
- [#652](https://github.com/Schniz/fnm/pull/652) Fix `fnm completions` ([@Schniz](https://github.com/Schniz))
#### Committers: 1
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
## v1.30.0 (2022-01-30)
#### Bugfix 🐛
- [#625](https://github.com/Schniz/fnm/pull/625) Revert from using sysinfo, but only on Unix machines ([@Schniz](https://github.com/Schniz))
- [#638](https://github.com/Schniz/fnm/pull/638) Use local cache directory instead of temp directory for symlinks ([@Schniz](https://github.com/Schniz))
#### Internal 🛠
- [#617](https://github.com/Schniz/fnm/pull/617) fix(deps): update rust crate clap to v3 ([@renovate[bot]](https://github.com/apps/renovate))
- [#630](https://github.com/Schniz/fnm/pull/630) Replace `snafu` with `thiserror` ([@Schniz](https://github.com/Schniz))
#### Documentation 📝
- [#637](https://github.com/Schniz/fnm/pull/637) [Documentation] Adds Additional info regarding .node-version ([@uchihamalolan](https://github.com/uchihamalolan))
#### Committers: 2
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
- Malolan B ([@uchihamalolan](https://github.com/uchihamalolan))
## v1.29.2 (2022-01-06)
#### Bugfix 🐛
- [#626](https://github.com/Schniz/fnm/pull/626) Create the multishells directory if it is missing ([@Schniz](https://github.com/Schniz))
#### Documentation 📝
- [#598](https://github.com/Schniz/fnm/pull/598) Update README.md file emoji ([@lorensr](https://github.com/lorensr))
- [#616](https://github.com/Schniz/fnm/pull/616) Use on cd by default ([@matthieubosquet](https://github.com/matthieubosquet))
#### Committers: 3
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
- Loren ☺ ([@lorensr](https://github.com/lorensr))
- Matthieu Bosquet ([@matthieubosquet](https://github.com/matthieubosquet))
## v1.29.1 (2021-12-28)
#### Bugfix 🐛
- [#613](https://github.com/Schniz/fnm/pull/613) Don't warn on `~/.fnm` yet ([@Schniz](https://github.com/Schniz))
#### Committers: 1
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
## v1.29.0 (2021-12-28)
#### New Feature 🎉
- [#607](https://github.com/Schniz/fnm/pull/607) Allow recursive version lookups ([@Schniz](https://github.com/Schniz))
- [#416](https://github.com/Schniz/fnm/pull/416) Respect \$XDG_DATA_HOME ([@samhh](https://github.com/samhh))
#### Bugfix 🐛
- [#603](https://github.com/Schniz/fnm/pull/603) (re)Include feature for (native) certificates ([@pfiaux](https://github.com/pfiaux))
- [#605](https://github.com/Schniz/fnm/pull/605) Add a user-agent header ([@Schniz](https://github.com/Schniz))
#### Internal 🛠
- [#606](https://github.com/Schniz/fnm/pull/606) Migrate to Rust 2021 edition ([@Schniz](https://github.com/Schniz))
#### Committers: 3
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
- Patrick Fiaux ([@pfiaux](https://github.com/pfiaux))
- Sam A. Horvath-Hunt ([@samhh](https://github.com/samhh))
## v1.28.2 (2021-12-05)
#### Bugfix 🐛 #### Bugfix 🐛
@ -197,7 +295,7 @@
#### Bugfix 🐛 #### Bugfix 🐛
- [#368](https://github.com/Schniz/fnm/pull/368) Update 'ring' to '0.16.17' ([@gucheen](https://github.com/gucheen)) - [#368](https://github.com/Schniz/fnm/pull/368) Update 'ring' to '0.16.17' ([@gucheen](https://github.com/gucheen))
- [#347](https://github.com/Schniz/fnm/pull/347) Check if $ZDOTDIR exists to find correct path to .zshrc ([@thales-maciel](https://github.com/thales-maciel)) - [#347](https://github.com/Schniz/fnm/pull/347) Check if \$ZDOTDIR exists to find correct path to .zshrc ([@thales-maciel](https://github.com/thales-maciel))
#### Documentation 📝 #### Documentation 📝

1393
Cargo.lock generated

File diff suppressed because it is too large Load Diff

45
Cargo.toml

@ -1,6 +1,6 @@
[package] [package]
name = "fnm" name = "fnm"
version = "1.28.2" version = "1.31.1"
authors = ["Gal Schlezinger <gal@spitfire.co.il>"] authors = ["Gal Schlezinger <gal@spitfire.co.il>"]
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
@ -9,38 +9,39 @@ repository = "https://github.com/Schniz/fnm"
description = "Fast and simple Node.js version manager" description = "Fast and simple Node.js version manager"
[dependencies] [dependencies]
serde = { version = "1.0.132", features = ["derive"] } serde = { version = "1.0.145", features = ["derive"] }
clap = "2.34.0" clap = { version = "3.2.23", features = ["derive", "env"] }
structopt = "0.3.25" serde_json = "1.0.85"
serde_json = "1.0.73" chrono = { version = "0.4.23", features = ["serde"] }
chrono = { version = "0.4.19", features = ["serde"] }
tar = "0.4.38" tar = "0.4.38"
xz2 = "0.1.6" xz2 = "0.1.7"
semver = "1.0.4" semver = "1.0.14"
dirs = "4.0.0" dirs = "4.0.0"
colored = "2.0.0" colored = "2.0.0"
zip = "0.5.13" zip = "0.6.3"
tempfile = "3.2.0" tempfile = "3.3.0"
indoc = "1.0.3" indoc = "1.0.7"
snafu = { version = "0.6.10", features = ["backtrace"] } log = "0.4.17"
log = "0.4.14" env_logger = "0.9.3"
env_logger = "0.9.0"
atty = "0.2.14" atty = "0.2.14"
encoding_rs_io = "0.1.7" encoding_rs_io = "0.1.7"
reqwest = { version = "0.11.7", features = ["blocking", "json", "rustls-tls", "brotli"], default-features = false } reqwest = { version = "0.11.12", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "brotli"], default-features = false }
url = "2.2.2" url = "2.3.1"
sysinfo = "0.22.3" sysinfo = "0.26.7"
thiserror = "1.0.37"
clap_complete = "3.2.5"
anyhow = "1.0.65"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.0.0" pretty_assertions = "1.3.0"
duct = "0.13.5" duct = "0.13.5"
shell-escape = "0.1.5" shell-escape = "0.1.5"
insta = { version = "1.8.0", features = ["backtrace"] } insta = "1.21.0"
serial_test = "0.5.1" serial_test = "0.9.0"
test-log = "0.2.8" test-log = "0.2.11"
[build-dependencies] [build-dependencies]
embed-resource = "1.6.5" embed-resource = "1.7.3"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
csv = "1.1.6" csv = "1.1.6"

34
README.md

@ -18,13 +18,15 @@
:rocket: Built with speed in mind :rocket: Built with speed in mind
:thinking: Works with `.node-version` and `.nvmrc` files :open_file_folder: Works with `.node-version` and `.nvmrc` files
## Installation ## Installation
### Using a script (macOS/Linux) ### Using a script (macOS/Linux)
For `bash`, `zsh` and `fish` shells, there's an [automatic installation script](./.ci/install.sh): For `bash`, `zsh` and `fish` shells, there's an [automatic installation script](./.ci/install.sh).
First ensure that `curl` and `unzip` are already installed on you operating system. Then execute:
```sh ```sh
curl -fsSL https://fnm.vercel.app/install | bash curl -fsSL https://fnm.vercel.app/install | bash
@ -118,31 +120,41 @@ Please follow your shell instructions to install them.
### Shell Setup ### Shell Setup
fnm needs to run some shell commands before you can start using it. Environment variables need to be setup before you can start using fnm.
This is done by evaluating the output of `fnm env`. Check out the following guides for the shell you use: This is done by evaluating the output of `fnm env`.
To automatically run `fnm use` when a directory contains a `.node-version` or `.nvmrc` file, add the `--use-on-cd` option to your shell setup.
Adding a `.node-version` to your project is as simple as:
```bash
$ node --version
v14.18.3
$ node --version > .node-version
```
Check out the following guides for the shell you use:
#### Bash #### Bash
add the following to your `.bashrc` profile: Add the following to your `.bashrc` profile:
```bash ```bash
eval "$(fnm env)" eval "$(fnm env --use-on-cd)"
``` ```
#### Zsh #### Zsh
add the following to your `.zshrc` profile: Add the following to your `.zshrc` profile:
```zsh ```zsh
eval "$(fnm env)" eval "$(fnm env --use-on-cd)"
``` ```
#### Fish shell #### Fish shell
create `~/.config/fish/conf.d/fnm.fish` add this line to it: Create `~/.config/fish/conf.d/fnm.fish` add this line to it:
```fish ```fish
fnm env | source fnm env --use-on-cd | source
``` ```
#### PowerShell #### PowerShell
@ -167,7 +179,7 @@ FOR /f "tokens=*" %i IN ('fnm env --use-on-cd') DO CALL %i
#### Usage with Cmder #### Usage with Cmder
Usage is very similar to the normal WinCMD install, apart for a few tweaks to allow being called from the cmder startup script. The example **assumes** that the `CMDER_ROOT` environment variable is **set** to the **root directory** of your Cmder installation. Usage is very similar to the normal WinCMD install, apart for a few tweaks to allow being called from the cmder startup script. The example **assumes** that the `CMDER_ROOT` environment variable is **set** to the **root directory** of your Cmder installation.
Then you can do something like this: Then you can do something like this:
- Make a .cmd file to invoke it - Make a .cmd file to invoke it

802
docs/commands.md

File diff suppressed because it is too large Load Diff

2
docs/fnm.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

269
e2e/__snapshots__/aliases.test.ts.snap

@ -0,0 +1,269 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash aliasing versions: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install 6.11.3
fnm install 8.11.3
fnm alias 8.11 oldie
fnm alias 6 older
fnm default older
((fnm ls) | grep 8.11.3 || (echo "Expected output to contain 8.11.3" && exit 1)) | grep oldie || (echo "Expected output to contain oldie" && exit 1)
((fnm ls) | grep 6.11.3 || (echo "Expected output to contain 6.11.3" && exit 1)) | grep older || (echo "Expected output to contain older" && exit 1)
fnm use older
if [ "$(node --version)" != "v6.11.3" ]; then
echo "Expected node version to be v6.11.3. Got $(node --version)"
exit 1
fi
fnm use oldie
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi
fnm use default
if [ "$(node --version)" != "v6.11.3" ]; then
echo "Expected node version to be v6.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash allows to install an lts if version missing: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm use --install-if-missing
(fnm ls) | grep lts-latest || (echo "Expected output to contain lts-latest" && exit 1)"
`;
exports[`Bash can alias the system node: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm alias system my_system
(fnm ls) | grep my_system || (echo "Expected output to contain my_system" && exit 1)
fnm alias system default
fnm alias my_system my_system2
(fnm ls) | grep my_system2 || (echo "Expected output to contain my_system2" && exit 1)
(fnm use my_system) | grep 'Bypassing fnm' || (echo "Expected output to contain 'Bypassing fnm'" && exit 1)
fnm unalias my_system
(fnm use my_system 2>&1) | grep 'Requested version my_system is not currently installed' || (echo "Expected output to contain 'Requested version my_system is not currently installed'" && exit 1)"
`;
exports[`Bash errors when alias is not found: Bash 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
(fnm use 2>&1) | grep 'Requested version lts-latest is not' || (echo "Expected output to contain 'Requested version lts-latest is not'" && exit 1)"
`;
exports[`Bash unalias a version: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install 11.10.0
fnm install 8.11.3
fnm alias 8.11.3 version8
(fnm ls) | grep version8 || (echo "Expected output to contain version8" && exit 1)
fnm unalias version8
(fnm use version8 2>&1) | grep 'Requested version version8 is not currently installed' || (echo "Expected output to contain 'Requested version version8 is not currently installed'" && exit 1)"
`;
exports[`Bash unalias errors if alias not found: Bash 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
(fnm unalias lts 2>&1) | grep 'Requested alias lts not found' || (echo "Expected output to contain 'Requested alias lts not found'" && exit 1)"
`;
exports[`Fish aliasing versions: Fish 1`] = `
"fnm env | source
fnm install 6.11.3
fnm install 8.11.3
fnm alias 8.11 oldie
fnm alias 6 older
fnm default older
begin; begin; fnm ls; end | grep 8.11.3; or echo "Expected output to contain 8.11.3" && exit 1; end | grep oldie; or echo "Expected output to contain oldie" && exit 1
begin; begin; fnm ls; end | grep 6.11.3; or echo "Expected output to contain 6.11.3" && exit 1; end | grep older; or echo "Expected output to contain older" && exit 1
fnm use older
set ____test____ (node --version)
if test "$____test____" != "v6.11.3"
echo "Expected node version to be v6.11.3. Got $____test____"
exit 1
end
fnm use oldie
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end
fnm use default
set ____test____ (node --version)
if test "$____test____" != "v6.11.3"
echo "Expected node version to be v6.11.3. Got $____test____"
exit 1
end"
`;
exports[`Fish allows to install an lts if version missing: Fish 1`] = `
"fnm env | source
fnm use --install-if-missing
begin; fnm ls; end | grep lts-latest; or echo "Expected output to contain lts-latest" && exit 1"
`;
exports[`Fish can alias the system node: Fish 1`] = `
"fnm env | source
fnm alias system my_system
begin; fnm ls; end | grep my_system; or echo "Expected output to contain my_system" && exit 1
fnm alias system default
fnm alias my_system my_system2
begin; fnm ls; end | grep my_system2; or echo "Expected output to contain my_system2" && exit 1
begin; fnm use my_system; end | grep 'Bypassing fnm'; or echo "Expected output to contain 'Bypassing fnm'" && exit 1
fnm unalias my_system
begin; fnm use my_system 2>&1; end | grep 'Requested version my_system is not currently installed'; or echo "Expected output to contain 'Requested version my_system is not currently installed'" && exit 1"
`;
exports[`Fish errors when alias is not found: Fish 1`] = `
"fnm env --log-level=error | source
begin; fnm use 2>&1; end | grep 'Requested version lts-latest is not'; or echo "Expected output to contain 'Requested version lts-latest is not'" && exit 1"
`;
exports[`Fish unalias a version: Fish 1`] = `
"fnm env | source
fnm install 11.10.0
fnm install 8.11.3
fnm alias 8.11.3 version8
begin; fnm ls; end | grep version8; or echo "Expected output to contain version8" && exit 1
fnm unalias version8
begin; fnm use version8 2>&1; end | grep 'Requested version version8 is not currently installed'; or echo "Expected output to contain 'Requested version version8 is not currently installed'" && exit 1"
`;
exports[`Fish unalias errors if alias not found: Fish 1`] = `
"fnm env --log-level=error | source
begin; fnm unalias lts 2>&1; end | grep 'Requested alias lts not found'; or echo "Expected output to contain 'Requested alias lts not found'" && exit 1"
`;
exports[`PowerShell aliasing versions: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install 6.11.3
fnm install 8.11.3
fnm alias 8.11 oldie
fnm alias 6 older
fnm default older
$($__out__ = $($($__out__ = $(fnm ls | Select-String 8.11.3); if ($__out__ -eq $null) { exit 1 } else { $__out__ }) | Select-String oldie); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
$($__out__ = $($($__out__ = $(fnm ls | Select-String 6.11.3); if ($__out__ -eq $null) { exit 1 } else { $__out__ }) | Select-String older); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm use older
if ( "$(node --version)" -ne "v6.11.3" ) { echo "Expected node version to be v6.11.3. Got $(node --version)"; exit 1 }
fnm use oldie
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }
fnm use default
if ( "$(node --version)" -ne "v6.11.3" ) { echo "Expected node version to be v6.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell allows to install an lts if version missing: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm use --install-if-missing
$($__out__ = $(fnm ls | Select-String lts-latest); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`PowerShell can alias the system node: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm alias system my_system
$($__out__ = $(fnm ls | Select-String my_system); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm alias system default
fnm alias my_system my_system2
$($__out__ = $(fnm ls | Select-String my_system2); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
$($__out__ = $(fnm use my_system | Select-String 'Bypassing fnm'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm unalias my_system
$($__out__ = $(fnm use my_system 2>&1 | Select-String 'Requested version my_system is not currently installed'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`PowerShell errors when alias is not found: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --log-level=error | Out-String | Invoke-Expression
$($__out__ = $(fnm use 2>&1 | Select-String 'Requested version lts-latest is not'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`PowerShell unalias a version: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install 11.10.0
fnm install 8.11.3
fnm alias 8.11.3 version8
$($__out__ = $(fnm ls | Select-String version8); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm unalias version8
$($__out__ = $(fnm use version8 2>&1 | Select-String 'Requested version version8 is not currently installed'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`PowerShell unalias errors if alias not found: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --log-level=error | Out-String | Invoke-Expression
$($__out__ = $(fnm unalias lts 2>&1 | Select-String 'Requested alias lts not found'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`Zsh aliasing versions: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install 6.11.3
fnm install 8.11.3
fnm alias 8.11 oldie
fnm alias 6 older
fnm default older
((fnm ls) | grep 8.11.3 || (echo "Expected output to contain 8.11.3" && exit 1)) | grep oldie || (echo "Expected output to contain oldie" && exit 1)
((fnm ls) | grep 6.11.3 || (echo "Expected output to contain 6.11.3" && exit 1)) | grep older || (echo "Expected output to contain older" && exit 1)
fnm use older
if [ "$(node --version)" != "v6.11.3" ]; then
echo "Expected node version to be v6.11.3. Got $(node --version)"
exit 1
fi
fnm use oldie
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi
fnm use default
if [ "$(node --version)" != "v6.11.3" ]; then
echo "Expected node version to be v6.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh allows to install an lts if version missing: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm use --install-if-missing
(fnm ls) | grep lts-latest || (echo "Expected output to contain lts-latest" && exit 1)"
`;
exports[`Zsh can alias the system node: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm alias system my_system
(fnm ls) | grep my_system || (echo "Expected output to contain my_system" && exit 1)
fnm alias system default
fnm alias my_system my_system2
(fnm ls) | grep my_system2 || (echo "Expected output to contain my_system2" && exit 1)
(fnm use my_system) | grep 'Bypassing fnm' || (echo "Expected output to contain 'Bypassing fnm'" && exit 1)
fnm unalias my_system
(fnm use my_system 2>&1) | grep 'Requested version my_system is not currently installed' || (echo "Expected output to contain 'Requested version my_system is not currently installed'" && exit 1)"
`;
exports[`Zsh errors when alias is not found: Zsh 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
(fnm use 2>&1) | grep 'Requested version lts-latest is not' || (echo "Expected output to contain 'Requested version lts-latest is not'" && exit 1)"
`;
exports[`Zsh unalias a version: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install 11.10.0
fnm install 8.11.3
fnm alias 8.11.3 version8
(fnm ls) | grep version8 || (echo "Expected output to contain version8" && exit 1)
fnm unalias version8
(fnm use version8 2>&1) | grep 'Requested version version8 is not currently installed' || (echo "Expected output to contain 'Requested version version8 is not currently installed'" && exit 1)"
`;
exports[`Zsh unalias errors if alias not found: Zsh 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
(fnm unalias lts 2>&1) | grep 'Requested alias lts not found' || (echo "Expected output to contain 'Requested alias lts not found'" && exit 1)"
`;

284
e2e/__snapshots__/basic.test.ts.snap

@ -0,0 +1,284 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash .node-version: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash .nvmrc: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash \`fnm ls\` with nothing installed: Bash 1`] = `
"set -e
eval "$(fnm env)"
if [ "$(fnm ls)" != "* system" ]; then
echo "Expected fnm ls to be * system. Got $(fnm ls)"
exit 1
fi"
`;
exports[`Bash basic usage: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
fnm use v8.11.3
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash resolves partial semver: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install 6
fnm use 6
if [ "$(node --version)" != "v6.17.1" ]; then
echo "Expected node version to be v6.17.1. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash use on cd: Bash 1`] = `
"set -e
eval "$(fnm env --use-on-cd)"
fnm install v8.11.3
fnm install v12.22.12
cd subdir
if [ "$(node --version)" != "v12.22.12" ]; then
echo "Expected node version to be v12.22.12. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash when .node-version and .nvmrc are in sync, it throws no error: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v11.10.0" ]; then
echo "Expected node version to be v11.10.0. Got $(node --version)"
exit 1
fi"
`;
exports[`Fish .node-version: Fish 1`] = `
"fnm env | source
fnm install
fnm use
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end"
`;
exports[`Fish .nvmrc: Fish 1`] = `
"fnm env | source
fnm install
fnm use
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end"
`;
exports[`Fish \`fnm ls\` with nothing installed: Fish 1`] = `
"fnm env | source
set ____test____ (fnm ls)
if test "$____test____" != "* system"
echo "Expected fnm ls to be * system. Got $____test____"
exit 1
end"
`;
exports[`Fish basic usage: Fish 1`] = `
"fnm env | source
fnm install v8.11.3
fnm use v8.11.3
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end"
`;
exports[`Fish resolves partial semver: Fish 1`] = `
"fnm env | source
fnm install 6
fnm use 6
set ____test____ (node --version)
if test "$____test____" != "v6.17.1"
echo "Expected node version to be v6.17.1. Got $____test____"
exit 1
end"
`;
exports[`Fish use on cd: Fish 1`] = `
"fnm env --use-on-cd | source
fnm install v8.11.3
fnm install v12.22.12
cd subdir
set ____test____ (node --version)
if test "$____test____" != "v12.22.12"
echo "Expected node version to be v12.22.12. Got $____test____"
exit 1
end"
`;
exports[`Fish when .node-version and .nvmrc are in sync, it throws no error: Fish 1`] = `
"fnm env | source
fnm install
fnm use
set ____test____ (node --version)
if test "$____test____" != "v11.10.0"
echo "Expected node version to be v11.10.0. Got $____test____"
exit 1
end"
`;
exports[`PowerShell .node-version: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install
fnm use
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell .nvmrc: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install
fnm use
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell \`fnm ls\` with nothing installed: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
if ( "$(fnm ls)" -ne "* system" ) { echo "Expected fnm ls to be * system. Got $(fnm ls)"; exit 1 }"
`;
exports[`PowerShell basic usage: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install v8.11.3
fnm use v8.11.3
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell resolves partial semver: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install 6
fnm use 6
if ( "$(node --version)" -ne "v6.17.1" ) { echo "Expected node version to be v6.17.1. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell use on cd: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --use-on-cd | Out-String | Invoke-Expression
fnm install v8.11.3
fnm install v12.22.12
cd subdir
if ( "$(node --version)" -ne "v12.22.12" ) { echo "Expected node version to be v12.22.12. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell when .node-version and .nvmrc are in sync, it throws no error: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install
fnm use
if ( "$(node --version)" -ne "v11.10.0" ) { echo "Expected node version to be v11.10.0. Got $(node --version)"; exit 1 }"
`;
exports[`Zsh .node-version: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh .nvmrc: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh \`fnm ls\` with nothing installed: Zsh 1`] = `
"set -e
eval "$(fnm env)"
if [ "$(fnm ls)" != "* system" ]; then
echo "Expected fnm ls to be * system. Got $(fnm ls)"
exit 1
fi"
`;
exports[`Zsh basic usage: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
fnm use v8.11.3
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh resolves partial semver: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install 6
fnm use 6
if [ "$(node --version)" != "v6.17.1" ]; then
echo "Expected node version to be v6.17.1. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh use on cd: Zsh 1`] = `
"set -e
eval "$(fnm env --use-on-cd)"
fnm install v8.11.3
fnm install v12.22.12
cd subdir
if [ "$(node --version)" != "v12.22.12" ]; then
echo "Expected node version to be v12.22.12. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh when .node-version and .nvmrc are in sync, it throws no error: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v11.10.0" ]; then
echo "Expected node version to be v11.10.0. Got $(node --version)"
exit 1
fi"
`;

96
e2e/__snapshots__/current.test.ts.snap

@ -0,0 +1,96 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash current returns the current Node.js version set in fnm: Bash 1`] = `
"set -e
eval "$(fnm env)"
if [ "$(fnm current)" != "none" ]; then
echo "Expected currently activated version to be none. Got $(fnm current)"
exit 1
fi
fnm install v8.11.3
fnm install v10.10.0
fnm use v8.11.3
if [ "$(fnm current)" != "v8.11.3" ]; then
echo "Expected currently activated version to be v8.11.3. Got $(fnm current)"
exit 1
fi
fnm use v10.10.0
if [ "$(fnm current)" != "v10.10.0" ]; then
echo "Expected currently activated version to be v10.10.0. Got $(fnm current)"
exit 1
fi
fnm use system
if [ "$(fnm current)" != "system" ]; then
echo "Expected currently activated version to be system. Got $(fnm current)"
exit 1
fi"
`;
exports[`Fish current returns the current Node.js version set in fnm: Fish 1`] = `
"fnm env | source
set ____test____ (fnm current)
if test "$____test____" != "none"
echo "Expected currently activated version to be none. Got $____test____"
exit 1
end
fnm install v8.11.3
fnm install v10.10.0
fnm use v8.11.3
set ____test____ (fnm current)
if test "$____test____" != "v8.11.3"
echo "Expected currently activated version to be v8.11.3. Got $____test____"
exit 1
end
fnm use v10.10.0
set ____test____ (fnm current)
if test "$____test____" != "v10.10.0"
echo "Expected currently activated version to be v10.10.0. Got $____test____"
exit 1
end
fnm use system
set ____test____ (fnm current)
if test "$____test____" != "system"
echo "Expected currently activated version to be system. Got $____test____"
exit 1
end"
`;
exports[`PowerShell current returns the current Node.js version set in fnm: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
if ( "$(fnm current)" -ne "none" ) { echo "Expected currently activated version to be none. Got $(fnm current)"; exit 1 }
fnm install v8.11.3
fnm install v10.10.0
fnm use v8.11.3
if ( "$(fnm current)" -ne "v8.11.3" ) { echo "Expected currently activated version to be v8.11.3. Got $(fnm current)"; exit 1 }
fnm use v10.10.0
if ( "$(fnm current)" -ne "v10.10.0" ) { echo "Expected currently activated version to be v10.10.0. Got $(fnm current)"; exit 1 }
fnm use system
if ( "$(fnm current)" -ne "system" ) { echo "Expected currently activated version to be system. Got $(fnm current)"; exit 1 }"
`;
exports[`Zsh current returns the current Node.js version set in fnm: Zsh 1`] = `
"set -e
eval "$(fnm env)"
if [ "$(fnm current)" != "none" ]; then
echo "Expected currently activated version to be none. Got $(fnm current)"
exit 1
fi
fnm install v8.11.3
fnm install v10.10.0
fnm use v8.11.3
if [ "$(fnm current)" != "v8.11.3" ]; then
echo "Expected currently activated version to be v8.11.3. Got $(fnm current)"
exit 1
fi
fnm use v10.10.0
if [ "$(fnm current)" != "v10.10.0" ]; then
echo "Expected currently activated version to be v10.10.0. Got $(fnm current)"
exit 1
fi
fnm use system
if [ "$(fnm current)" != "system" ]; then
echo "Expected currently activated version to be system. Got $(fnm current)"
exit 1
fi"
`;

18
e2e/__snapshots__/env.test.ts.snap

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash outputs json: Bash 1`] = `
"set -e
fnm env --json > file.json"
`;
exports[`Fish outputs json: Fish 1`] = `"fnm env --json > file.json"`;
exports[`PowerShell outputs json: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --json | Out-File file.json -Encoding UTF8"
`;
exports[`Zsh outputs json: Zsh 1`] = `
"set -e
fnm env --json > file.json"
`;

70
e2e/__snapshots__/exec.test.ts.snap

@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash \`exec\` usage: Bash 1`] = `
"set -e
fnm install
fnm install v6.10.0
fnm install v10.10.0
if [ "$(fnm exec -- node -v)" != "v8.10.0" ]; then
echo "Expected version file exec to be v8.10.0. Got $(fnm exec -- node -v)"
exit 1
fi
if [ "$(fnm exec --using=6 -- node -v)" != "v6.10.0" ]; then
echo "Expected exec:6 node -v to be v6.10.0. Got $(fnm exec --using=6 -- node -v)"
exit 1
fi
if [ "$(fnm exec --using=10 -- node -v)" != "v10.10.0" ]; then
echo "Expected exec:6 node -v to be v10.10.0. Got $(fnm exec --using=10 -- node -v)"
exit 1
fi"
`;
exports[`Fish \`exec\` usage: Fish 1`] = `
"fnm install
fnm install v6.10.0
fnm install v10.10.0
set ____test____ (fnm exec -- node -v)
if test "$____test____" != "v8.10.0"
echo "Expected version file exec to be v8.10.0. Got $____test____"
exit 1
end
set ____test____ (fnm exec --using=6 -- node -v)
if test "$____test____" != "v6.10.0"
echo "Expected exec:6 node -v to be v6.10.0. Got $____test____"
exit 1
end
set ____test____ (fnm exec --using=10 -- node -v)
if test "$____test____" != "v10.10.0"
echo "Expected exec:6 node -v to be v10.10.0. Got $____test____"
exit 1
end"
`;
exports[`PowerShell \`exec\` usage: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm install
fnm install v6.10.0
fnm install v10.10.0
if ( "$(fnm exec -- node -v)" -ne "v8.10.0" ) { echo "Expected version file exec to be v8.10.0. Got $(fnm exec -- node -v)"; exit 1 }
if ( "$(fnm exec --using=6 -- node -v)" -ne "v6.10.0" ) { echo "Expected exec:6 node -v to be v6.10.0. Got $(fnm exec --using=6 -- node -v)"; exit 1 }
if ( "$(fnm exec --using=10 -- node -v)" -ne "v10.10.0" ) { echo "Expected exec:6 node -v to be v10.10.0. Got $(fnm exec --using=10 -- node -v)"; exit 1 }"
`;
exports[`Zsh \`exec\` usage: Zsh 1`] = `
"set -e
fnm install
fnm install v6.10.0
fnm install v10.10.0
if [ "$(fnm exec -- node -v)" != "v8.10.0" ]; then
echo "Expected version file exec to be v8.10.0. Got $(fnm exec -- node -v)"
exit 1
fi
if [ "$(fnm exec --using=6 -- node -v)" != "v6.10.0" ]; then
echo "Expected exec:6 node -v to be v6.10.0. Got $(fnm exec --using=6 -- node -v)"
exit 1
fi
if [ "$(fnm exec --using=10 -- node -v)" != "v10.10.0" ]; then
echo "Expected exec:6 node -v to be v10.10.0. Got $(fnm exec --using=10 -- node -v)"
exit 1
fi"
`;

28
e2e/__snapshots__/existing-installation.test.ts.snap

@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash warns about an existing installation: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
(fnm install v8.11.3 2>&1) | grep 'already installed' || (echo "Expected output to contain 'already installed'" && exit 1)"
`;
exports[`Fish warns about an existing installation: Fish 1`] = `
"fnm env | source
fnm install v8.11.3
begin; fnm install v8.11.3 2>&1; end | grep 'already installed'; or echo "Expected output to contain 'already installed'" && exit 1"
`;
exports[`PowerShell warns about an existing installation: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install v8.11.3
$($__out__ = $(fnm install v8.11.3 2>&1 | Select-String 'already installed'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`Zsh warns about an existing installation: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
(fnm install v8.11.3 2>&1) | grep 'already installed' || (echo "Expected output to contain 'already installed'" && exit 1)"
`;

32
e2e/__snapshots__/latest-lts.test.ts.snap

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash installs latest lts: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install --lts
(fnm ls) | grep lts-latest || (echo "Expected output to contain lts-latest" && exit 1)
fnm use 'lts/*'"
`;
exports[`Fish installs latest lts: Fish 1`] = `
"fnm env | source
fnm install --lts
begin; fnm ls; end | grep lts-latest; or echo "Expected output to contain lts-latest" && exit 1
fnm use 'lts/*'"
`;
exports[`PowerShell installs latest lts: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install --lts
$($__out__ = $(fnm ls | Select-String lts-latest); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm use 'lts/*'"
`;
exports[`Zsh installs latest lts: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install --lts
(fnm ls) | grep lts-latest || (echo "Expected output to contain lts-latest" && exit 1)
fnm use 'lts/*'"
`;

127
e2e/__snapshots__/log-level.test.ts.snap

@ -0,0 +1,127 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash "quiet" log level: Bash 1`] = `
"set -e
eval "$(fnm env --log-level=quiet)"
if [ "$(fnm install v8.11.3)" != "" ]; then
echo "Expected fnm install to be . Got $(fnm install v8.11.3)"
exit 1
fi
if [ "$(fnm use v8.11.3)" != "" ]; then
echo "Expected fnm use to be . Got $(fnm use v8.11.3)"
exit 1
fi
if [ "$(fnm alias v8.11.3 something)" != "" ]; then
echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"
exit 1
fi"
`;
exports[`Bash error log level: Bash 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
if [ "$(fnm install v8.11.3)" != "" ]; then
echo "Expected fnm install to be . Got $(fnm install v8.11.3)"
exit 1
fi
if [ "$(fnm use v8.11.3)" != "" ]; then
echo "Expected fnm use to be . Got $(fnm use v8.11.3)"
exit 1
fi
if [ "$(fnm alias v8.11.3 something)" != "" ]; then
echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"
exit 1
fi
(fnm alias abcd efg 2>&1) | grep "find requested version" || (echo "Expected output to contain "find requested version"" && exit 1)"
`;
exports[`Fish "quiet" log level: Fish 1`] = `
"fnm env --log-level=quiet | source
set ____test____ (fnm install v8.11.3)
if test "$____test____" != ""
echo "Expected fnm install to be . Got $____test____"
exit 1
end
set ____test____ (fnm use v8.11.3)
if test "$____test____" != ""
echo "Expected fnm use to be . Got $____test____"
exit 1
end
set ____test____ (fnm alias v8.11.3 something)
if test "$____test____" != ""
echo "Expected fnm alias to be . Got $____test____"
exit 1
end"
`;
exports[`Fish error log level: Fish 1`] = `
"fnm env --log-level=error | source
set ____test____ (fnm install v8.11.3)
if test "$____test____" != ""
echo "Expected fnm install to be . Got $____test____"
exit 1
end
set ____test____ (fnm use v8.11.3)
if test "$____test____" != ""
echo "Expected fnm use to be . Got $____test____"
exit 1
end
set ____test____ (fnm alias v8.11.3 something)
if test "$____test____" != ""
echo "Expected fnm alias to be . Got $____test____"
exit 1
end
begin; fnm alias abcd efg 2>&1; end | grep "find requested version"; or echo "Expected output to contain "find requested version"" && exit 1"
`;
exports[`PowerShell "quiet" log level: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --log-level=quiet | Out-String | Invoke-Expression
if ( "$(fnm install v8.11.3)" -ne "" ) { echo "Expected fnm install to be . Got $(fnm install v8.11.3)"; exit 1 }
if ( "$(fnm use v8.11.3)" -ne "" ) { echo "Expected fnm use to be . Got $(fnm use v8.11.3)"; exit 1 }
if ( "$(fnm alias v8.11.3 something)" -ne "" ) { echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"; exit 1 }"
`;
exports[`PowerShell error log level: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --log-level=error | Out-String | Invoke-Expression
if ( "$(fnm install v8.11.3)" -ne "" ) { echo "Expected fnm install to be . Got $(fnm install v8.11.3)"; exit 1 }
if ( "$(fnm use v8.11.3)" -ne "" ) { echo "Expected fnm use to be . Got $(fnm use v8.11.3)"; exit 1 }
if ( "$(fnm alias v8.11.3 something)" -ne "" ) { echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"; exit 1 }
$($__out__ = $(fnm alias abcd efg 2>&1 | Select-String "find requested version"); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`Zsh "quiet" log level: Zsh 1`] = `
"set -e
eval "$(fnm env --log-level=quiet)"
if [ "$(fnm install v8.11.3)" != "" ]; then
echo "Expected fnm install to be . Got $(fnm install v8.11.3)"
exit 1
fi
if [ "$(fnm use v8.11.3)" != "" ]; then
echo "Expected fnm use to be . Got $(fnm use v8.11.3)"
exit 1
fi
if [ "$(fnm alias v8.11.3 something)" != "" ]; then
echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"
exit 1
fi"
`;
exports[`Zsh error log level: Zsh 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
if [ "$(fnm install v8.11.3)" != "" ]; then
echo "Expected fnm install to be . Got $(fnm install v8.11.3)"
exit 1
fi
if [ "$(fnm use v8.11.3)" != "" ]; then
echo "Expected fnm use to be . Got $(fnm use v8.11.3)"
exit 1
fi
if [ "$(fnm alias v8.11.3 something)" != "" ]; then
echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"
exit 1
fi
(fnm alias abcd efg 2>&1) | grep "find requested version" || (echo "Expected output to contain "find requested version"" && exit 1)"
`;

67
e2e/__snapshots__/multishell.test.ts.snap

@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash multishell changes don't affect parent: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
fnm install v11.9.0
echo 'set -e
eval "$(fnm env)"
fnm use v11
if [ "$(node --version)" != "v11.9.0" ]; then
echo "Expected node version to be v11.9.0. Got $(node --version)"
exit 1
fi' | bash
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Fish multishell changes don't affect parent: Fish 1`] = `
"fnm env | source
fnm install v8.11.3
fnm install v11.9.0
fish -c 'fnm env | source
fnm use v11
set ____test____ (node --version)
if test "$____test____" != "v11.9.0"
echo "Expected node version to be v11.9.0. Got $____test____"
exit 1
end'
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end"
`;
exports[`PowerShell multishell changes don't affect parent: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install v8.11.3
fnm install v11.9.0
echo '$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm use v11
if ( "$(node --version)" -ne "v11.9.0" ) { echo "Expected node version to be v11.9.0. Got $(node --version)"; exit 1 }' | pwsh -NoProfile
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`Zsh multishell changes don't affect parent: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
fnm install v11.9.0
echo 'set -e
eval "$(fnm env)"
fnm use v11
if [ "$(node --version)" != "v11.9.0" ]; then
echo "Expected node version to be v11.9.0. Got $(node --version)"
exit 1
fi' | zsh
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;

32
e2e/__snapshots__/nvmrc-lts.test.ts.snap

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash uses .nvmrc with lts definition: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
(fnm ls) | grep lts-dubnium || (echo "Expected output to contain lts-dubnium" && exit 1)"
`;
exports[`Fish uses .nvmrc with lts definition: Fish 1`] = `
"fnm env | source
fnm install
fnm use
begin; fnm ls; end | grep lts-dubnium; or echo "Expected output to contain lts-dubnium" && exit 1"
`;
exports[`PowerShell uses .nvmrc with lts definition: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install
fnm use
$($__out__ = $(fnm ls | Select-String lts-dubnium); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`Zsh uses .nvmrc with lts definition: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
(fnm ls) | grep lts-dubnium || (echo "Expected output to contain lts-dubnium" && exit 1)"
`;

59
e2e/__snapshots__/shims.test.ts.snap

@ -0,0 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash outputs json: Bash 1`] = `
"set -e
fnm env --json --with-shims > file.json"
`;
exports[`Bash runs Node through a shim: Bash 1`] = `
"set -e
eval "$(fnm env --with-shims)"
fnm install 12.0.0
fnm use 12.0.0
if [ "$(node --version)" != "v12.0.0" ]; then
echo "Expected node version to be v12.0.0. Got $(node --version)"
exit 1
fi"
`;
exports[`Fish outputs json: Fish 1`] = `"fnm env --json --with-shims > file.json"`;
exports[`Fish runs Node through a shim: Fish 1`] = `
"fnm env --with-shims | source
fnm install 12.0.0
fnm use 12.0.0
set ____test____ (node --version)
if test "$____test____" != "v12.0.0"
echo "Expected node version to be v12.0.0. Got $____test____"
exit 1
end"
`;
exports[`PowerShell outputs json: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --json --with-shims | Out-File file.json -Encoding UTF8"
`;
exports[`PowerShell runs Node through a shim: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --with-shims | Out-String | Invoke-Expression
fnm install 12.0.0
fnm use 12.0.0
if ( "$(node --version)" -ne "v12.0.0" ) { echo "Expected node version to be v12.0.0. Got $(node --version)"; exit 1 }"
`;
exports[`Zsh outputs json: Zsh 1`] = `
"set -e
fnm env --json --with-shims > file.json"
`;
exports[`Zsh runs Node through a shim: Zsh 1`] = `
"set -e
eval "$(fnm env --with-shims)"
fnm install 12.0.0
fnm use 12.0.0
if [ "$(node --version)" != "v12.0.0" ]; then
echo "Expected node version to be v12.0.0. Got $(node --version)"
exit 1
fi"
`;

46
e2e/__snapshots__/uninstall.test.ts.snap

@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash uninstalls a version: Bash 1`] = `
"set -e
fnm install 12.0.0
fnm alias 12.0.0 hello
((fnm ls) | grep v12.0.0 || (echo "Expected output to contain v12.0.0" && exit 1)) | grep hello || (echo "Expected output to contain hello" && exit 1)
fnm uninstall hello
if [ "$(fnm ls)" != "* system" ]; then
echo "Expected fnm ls to be * system. Got $(fnm ls)"
exit 1
fi"
`;
exports[`Fish uninstalls a version: Fish 1`] = `
"fnm install 12.0.0
fnm alias 12.0.0 hello
begin; begin; fnm ls; end | grep v12.0.0; or echo "Expected output to contain v12.0.0" && exit 1; end | grep hello; or echo "Expected output to contain hello" && exit 1
fnm uninstall hello
set ____test____ (fnm ls)
if test "$____test____" != "* system"
echo "Expected fnm ls to be * system. Got $____test____"
exit 1
end"
`;
exports[`PowerShell uninstalls a version: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm install 12.0.0
fnm alias 12.0.0 hello
$($__out__ = $($($__out__ = $(fnm ls | Select-String v12.0.0); if ($__out__ -eq $null) { exit 1 } else { $__out__ }) | Select-String hello); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm uninstall hello
if ( "$(fnm ls)" -ne "* system" ) { echo "Expected fnm ls to be * system. Got $(fnm ls)"; exit 1 }"
`;
exports[`Zsh uninstalls a version: Zsh 1`] = `
"set -e
fnm install 12.0.0
fnm alias 12.0.0 hello
((fnm ls) | grep v12.0.0 || (echo "Expected output to contain v12.0.0" && exit 1)) | grep hello || (echo "Expected output to contain hello" && exit 1)
fnm uninstall hello
if [ "$(fnm ls)" != "* system" ]; then
echo "Expected fnm ls to be * system. Got $(fnm ls)"
exit 1
fi"
`;

129
e2e/aliases.test.ts

@ -0,0 +1,129 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
import { writeFile } from "node:fs/promises"
import path from "node:path"
import testCwd from "./shellcode/test-cwd.js"
import getStderr from "./shellcode/get-stderr.js"
import testNodeVersion from "./shellcode/test-node-version.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`allows to install an lts if version missing`, async () => {
await writeFile(path.join(testCwd(), ".node-version"), "lts/*")
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["use", "--install-if-missing"]))
.then(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "lts-latest")
)
.takeSnapshot(shell)
.execute(shell)
})
test(`errors when alias is not found`, async () => {
await writeFile(path.join(testCwd(), ".node-version"), "lts/*")
await script(shell)
.then(shell.env({ logLevel: "error" }))
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["use"])),
"'Requested version lts-latest is not'"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test(`unalias a version`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "11.10.0"]))
.then(shell.call("fnm", ["install", "8.11.3"]))
.then(shell.call("fnm", ["alias", "8.11.3", "version8"]))
.then(shell.scriptOutputContains(shell.call("fnm", ["ls"]), "version8"))
.then(shell.call("fnm", ["unalias", "version8"]))
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["use", "version8"])),
"'Requested version version8 is not currently installed'"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test(`unalias errors if alias not found`, async () => {
await script(shell)
.then(shell.env({ logLevel: "error" }))
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["unalias", "lts"])),
"'Requested alias lts not found'"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test(`can alias the system node`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["alias", "system", "my_system"]))
.then(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "my_system")
)
.then(shell.call("fnm", ["alias", "system", "default"]))
.then(shell.call("fnm", ["alias", "my_system", "my_system2"]))
.then(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "my_system2")
)
.then(
shell.scriptOutputContains(
shell.call("fnm", ["use", "my_system"]),
"'Bypassing fnm'"
)
)
.then(shell.call("fnm", ["unalias", "my_system"]))
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["use", "my_system"])),
"'Requested version my_system is not currently installed'"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test(`aliasing versions`, async () => {
const installedVersions = shell.call("fnm", ["ls"])
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "6.11.3"]))
.then(shell.call("fnm", ["install", "8.11.3"]))
.then(shell.call("fnm", ["alias", "8.11", "oldie"]))
.then(shell.call("fnm", ["alias", "6", "older"]))
.then(shell.call("fnm", ["default", "older"]))
.then(
shell.scriptOutputContains(
shell.scriptOutputContains(installedVersions, "8.11.3"),
"oldie"
)
)
.then(
shell.scriptOutputContains(
shell.scriptOutputContains(installedVersions, "6.11.3"),
"older"
)
)
.then(shell.call("fnm", ["use", "older"]))
.then(testNodeVersion(shell, "v6.11.3"))
.then(shell.call("fnm", ["use", "oldie"]))
.then(testNodeVersion(shell, "v8.11.3"))
.then(shell.call("fnm", ["use", "default"]))
.then(testNodeVersion(shell, "v6.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
})
}

93
e2e/basic.test.ts

@ -0,0 +1,93 @@
import { writeFile, mkdir } from "node:fs/promises"
import { join } from "node:path"
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import testCwd from "./shellcode/test-cwd.js"
import testNodeVersion from "./shellcode/test-node-version.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
describe(shell, () => {
test(`basic usage`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "v8.11.3"]))
.then(shell.call("fnm", ["use", "v8.11.3"]))
.then(testNodeVersion(shell, "v8.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
test(`.nvmrc`, async () => {
await writeFile(join(testCwd(), ".nvmrc"), "v8.11.3")
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["use"]))
.then(testNodeVersion(shell, "v8.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
test(`.node-version`, async () => {
await writeFile(join(testCwd(), ".node-version"), "v8.11.3")
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["use"]))
.then(testNodeVersion(shell, "v8.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
test(`use on cd`, async () => {
await mkdir(join(testCwd(), "subdir"), { recursive: true })
await writeFile(join(testCwd(), "subdir", ".node-version"), "v12.22.12")
await script(shell)
.then(shell.env({ useOnCd: true }))
.then(shell.call("fnm", ["install", "v8.11.3"]))
.then(shell.call("fnm", ["install", "v12.22.12"]))
.then(shell.call("cd", ["subdir"]))
.then(testNodeVersion(shell, "v12.22.12"))
.takeSnapshot(shell)
.execute(shell)
})
test(`resolves partial semver`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "6"]))
.then(shell.call("fnm", ["use", "6"]))
.then(testNodeVersion(shell, "v6.17.1"))
.takeSnapshot(shell)
.execute(shell)
})
test("`fnm ls` with nothing installed", async () => {
await script(shell)
.then(shell.env({}))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["ls"]),
"* system",
"fnm ls"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test(`when .node-version and .nvmrc are in sync, it throws no error`, async () => {
await writeFile(join(testCwd(), ".nvmrc"), "v11.10.0")
await writeFile(join(testCwd(), ".node-version"), "v11.10.0")
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["use"]))
.then(testNodeVersion(shell, "v11.10.0"))
.takeSnapshot(shell)
.execute(shell)
})
})
}

47
e2e/current.test.ts

@ -0,0 +1,47 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
describe(shell, () => {
test(`current returns the current Node.js version set in fnm`, async () => {
await script(shell)
.then(shell.env({}))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["current"]),
"none",
"currently activated version"
)
)
.then(shell.call("fnm", ["install", "v8.11.3"]))
.then(shell.call("fnm", ["install", "v10.10.0"]))
.then(shell.call("fnm", ["use", "v8.11.3"]))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["current"]),
"v8.11.3",
"currently activated version"
)
)
.then(shell.call("fnm", ["use", "v10.10.0"]))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["current"]),
"v10.10.0",
"currently activated version"
)
)
.then(shell.call("fnm", ["use", "system"]))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["current"]),
"system",
"currently activated version"
)
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

15
e2e/describe.ts

@ -0,0 +1,15 @@
import { WinCmd } from "./shellcode/shells.js"
import { Shell } from "./shellcode/shells/types.js"
export default function describe(
shell: Pick<Shell, "name">,
fn: () => void
): void {
if (shell === WinCmd) {
// wincmd tests do not work right now and I don't have a Windows machine to fix it
// maybe in the future when I have some time to spin a VM to check what's happening.
return globalThis.describe.skip("WinCmd", fn)
}
globalThis.describe(shell.name(), fn)
}

35
e2e/env.test.ts

@ -0,0 +1,35 @@
import { readFile } from "node:fs/promises"
import { join } from "node:path"
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import testCwd from "./shellcode/test-cwd.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
describe(shell, () => {
test(`outputs json`, async () => {
const filename = `file.json`
await script(shell)
.then(
shell.redirectOutput(shell.call("fnm", ["env", "--json"]), {
output: filename,
})
)
.takeSnapshot(shell)
.execute(shell)
if (shell.currentlySupported()) {
const file = await readFile(join(testCwd(), filename), "utf8")
expect(JSON.parse(file)).toEqual({
FNM_ARCH: expect.any(String),
FNM_DIR: expect.any(String),
FNM_LOGLEVEL: "info",
FNM_MULTISHELL_PATH: expect.any(String),
FNM_NODE_DIST_MIRROR: expect.any(String),
FNM_VERSION_FILE_STRATEGY: "local",
FNM_VERSION_SWITCH_STRATEGY: "path-symlink",
})
}
})
})
}

42
e2e/exec.test.ts

@ -0,0 +1,42 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import testCwd from "./shellcode/test-cwd.js"
import fs from "node:fs/promises"
import path from "node:path"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
describe(shell, () => {
test("`exec` usage", async () => {
await fs.writeFile(path.join(testCwd(), ".nvmrc"), "v8.10.0")
await script(shell)
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["install", "v6.10.0"]))
.then(shell.call("fnm", ["install", "v10.10.0"]))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["exec", "--", "node", "-v"]),
"v8.10.0",
"version file exec"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["exec", "--using=6", "--", "node", "-v"]),
"v6.10.0",
"exec:6 node -v"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["exec", "--using=10", "--", "node", "-v"]),
"v10.10.0",
"exec:6 node -v"
)
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

22
e2e/existing-installation.test.ts

@ -0,0 +1,22 @@
import getStderr from "./shellcode/get-stderr.js"
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`warns about an existing installation`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "v8.11.3"]))
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["install", "v8.11.3"])),
"'already installed'"
)
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

19
e2e/latest-lts.test.ts

@ -0,0 +1,19 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`installs latest lts`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "--lts"]))
.then(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "lts-latest")
)
.then(shell.call("fnm", ["use", "'lts/*'"]))
.takeSnapshot(shell)
.execute(shell)
})
})
}

71
e2e/log-level.test.ts

@ -0,0 +1,71 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
import getStderr from "./shellcode/get-stderr.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`"quiet" log level`, async () => {
await script(shell)
.then(shell.env({ logLevel: "quiet" }))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["install", "v8.11.3"]),
"",
"fnm install"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["use", "v8.11.3"]),
"",
"fnm use"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["alias", "v8.11.3", "something"]),
"",
"fnm alias"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test("error log level", async () => {
await script(shell)
.then(shell.env({ logLevel: "error" }))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["install", "v8.11.3"]),
"",
"fnm install"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["use", "v8.11.3"]),
"",
"fnm use"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["alias", "v8.11.3", "something"]),
"",
"fnm alias"
)
)
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["alias", "abcd", "efg"])),
`"find requested version"`
)
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

27
e2e/multishell.test.ts

@ -0,0 +1,27 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import testNodeVersion from "./shellcode/test-node-version.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`multishell changes don't affect parent`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "v8.11.3"]))
.then(shell.call("fnm", ["install", "v11.9.0"]))
.then(
shell.inSubShell(
script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["use", "v11"]))
.then(testNodeVersion(shell, "v11.9.0"))
.asLine()
)
)
.then(testNodeVersion(shell, "v8.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
})
}

24
e2e/nvmrc-lts.test.ts

@ -0,0 +1,24 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import fs from "node:fs/promises"
import path from "node:path"
import describe from "./describe.js"
import testCwd from "./shellcode/test-cwd.js"
for (const shell of [Bash, Fish, PowerShell, Zsh]) {
describe(shell, () => {
test(`uses .nvmrc with lts definition`, async () => {
await fs.writeFile(path.join(testCwd(), ".nvmrc"), `lts/dubnium`)
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["use"]))
.then(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "lts-dubnium")
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

3
e2e/shellcode/get-stderr.ts

@ -0,0 +1,3 @@
export default function getStderr(script: string): string {
return `${script} 2>&1`
}

179
e2e/shellcode/script.ts

@ -0,0 +1,179 @@
import { ScriptLine, Shell } from "./shells/types.js"
import { execa, type ExecaChildProcess } from "execa"
import testTmpDir from "./test-tmp-dir.js"
import { Writable } from "node:stream"
import { dedent } from "ts-dedent"
import testCwd from "./test-cwd.js"
import path, { join } from "node:path"
import { writeFile } from "node:fs/promises"
import chalk from "chalk"
import testBinDir from "./test-bin-dir.js"
class Script {
constructor(
private readonly config: {
fnmDir: string
},
private readonly lines: ScriptLine[]
) {}
then(line: ScriptLine): Script {
return new Script(this.config, [...this.lines, line])
}
takeSnapshot(shell: Pick<Shell, "name">): this {
const script = this.lines.join("\n")
expect(script).toMatchSnapshot(shell.name())
return this
}
async execute(
shell: Pick<
Shell,
"binaryName" | "launchArgs" | "currentlySupported" | "forceFile"
>
): Promise<void> {
if (!shell.currentlySupported()) {
return
}
const args = [...shell.launchArgs()]
if (shell.forceFile) {
let filename = join(testTmpDir(), "script")
if (typeof shell.forceFile === "string") {
filename = filename + shell.forceFile
}
await writeFile(filename, [...this.lines, "exit 0"].join("\n"))
args.push(filename)
}
const child = execa(shell.binaryName(), args, {
stdio: [shell.forceFile ? "ignore" : "pipe", "pipe", "pipe"],
cwd: testCwd(),
env: (() => {
const newProcessEnv: Record<string, string> = {
...removeAllFnmEnvVars(process.env),
PATH: [testBinDir(), fnmTargetDir(), process.env.PATH]
.filter(Boolean)
.join(path.delimiter),
FNM_DIR: this.config.fnmDir,
}
delete newProcessEnv.NODE_OPTIONS
return newProcessEnv
})(),
extendEnv: false,
reject: false,
})
if (child.stdin) {
const childStdin = child.stdin
for (const line of this.lines) {
await write(childStdin, `${line}\n`)
}
await write(childStdin, "exit 0\n")
}
const { stdout, stderr } = streamOutputsAndBuffer(child)
const finished = await child
if (finished.failed) {
console.error(
dedent`
Script failed.
code ${finished.exitCode}
signal ${finished.signal}
stdout:
${padAllLines(stdout.join(""), 2)}
stderr:
${padAllLines(stderr.join(""), 2)}
`
)
throw new Error(
`Script failed on ${testCwd()} with code ${finished.exitCode}`
)
}
}
asLine(): ScriptLine {
return this.lines.join("\n")
}
}
function streamOutputsAndBuffer(child: ExecaChildProcess) {
const stdout: string[] = []
const stderr: string[] = []
const testName = expect.getState().currentTestName ?? "unknown"
const testPath = expect.getState().testPath ?? "unknown"
const stdoutPrefix = chalk.cyan.dim(`[stdout] ${testPath}/${testName}: `)
const stderrPrefix = chalk.magenta.dim(`[stderr] ${testPath}/${testName}: `)
if (child.stdout) {
child.stdout.on("data", (data) => {
const line = data.toString().trim()
if (line) {
process.stdout.write(`${stdoutPrefix}${line}\n`)
}
stdout.push(data.toString())
})
}
if (child.stderr) {
child.stderr.on("data", (data) => {
const line = data.toString().trim()
if (line) {
process.stdout.write(`${stderrPrefix}${line}\n`)
}
stderr.push(data.toString())
})
}
return { stdout, stderr }
}
function padAllLines(text: string, padding: number): string {
return text
.split("\n")
.map((line) => " ".repeat(padding) + line)
.join("\n")
}
function write(writable: Writable, text: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
writable.write(text, (err) => {
if (err) return reject(err)
return resolve()
})
})
}
export function script(shell: Pick<Shell, "dieOnErrors">): Script {
const fnmDir = path.join(testTmpDir(), "fnm")
return new Script({ fnmDir }, shell.dieOnErrors ? [shell.dieOnErrors()] : [])
}
function removeAllFnmEnvVars(obj: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const result: NodeJS.ProcessEnv = {}
for (const [key, value] of Object.entries(obj)) {
if (!key.startsWith("FNM_")) {
result[key] = value
}
}
return result
}
function fnmTargetDir(): string {
return path.join(
process.cwd(),
"target",
process.env.FNM_TARGET_NAME ?? "debug"
)
}

97
e2e/shellcode/shells.ts

@ -0,0 +1,97 @@
import { cmdCall } from "./shells/cmdCall.js"
import { cmdEnv } from "./shells/cmdEnv.js"
import { cmdExpectCommandOutput } from "./shells/expect-command-output.js"
import { cmdHasOutputContains } from "./shells/output-contains.js"
import { redirectOutput } from "./shells/redirect-output.js"
import { cmdInSubShell } from "./shells/sub-shell.js"
import { define, Shell } from "./shells/types.js"
export const Bash = {
...define<Shell>({
binaryName: () => "bash",
currentlySupported: () => true,
name: () => "Bash",
launchArgs: () => ["-i"],
escapeText: (x) => x,
dieOnErrors: () => `set -e`,
}),
...cmdEnv.bash,
...cmdCall.all,
...redirectOutput.bash,
...cmdExpectCommandOutput.bash,
...cmdHasOutputContains.bash,
...cmdInSubShell.bash,
}
export const Zsh = {
...define<Shell>({
binaryName: () => "zsh",
currentlySupported: () => process.platform !== "win32",
name: () => "Zsh",
launchArgs: () => [],
escapeText: (x) => x,
dieOnErrors: () => `set -e`,
}),
...cmdEnv.bash,
...cmdCall.all,
...redirectOutput.bash,
...cmdExpectCommandOutput.bash,
...cmdHasOutputContains.bash,
...cmdInSubShell.zsh,
}
export const Fish = {
...define<Shell>({
binaryName: () => "fish",
currentlySupported: () => process.platform !== "win32",
name: () => "Fish",
launchArgs: () => [],
escapeText: (x) => x,
forceFile: true,
}),
...cmdEnv.fish,
...cmdCall.all,
...redirectOutput.bash,
...cmdExpectCommandOutput.fish,
...cmdHasOutputContains.fish,
...cmdInSubShell.fish,
}
export const PowerShell = {
...define<Shell>({
binaryName: () => "pwsh",
forceFile: ".ps1",
currentlySupported: () => true,
name: () => "PowerShell",
launchArgs: () => ["-NoProfile"],
escapeText: (x) => x,
dieOnErrors: () => `$ErrorActionPreference = "Stop"`,
}),
...cmdEnv.powershell,
...cmdCall.all,
...redirectOutput.powershell,
...cmdExpectCommandOutput.powershell,
...cmdHasOutputContains.powershell,
...cmdInSubShell.powershell,
}
export const WinCmd = {
...define<Shell>({
binaryName: () => "cmd.exe",
currentlySupported: () => process.platform === "win32",
name: () => "Windows Command Prompt",
launchArgs: () => [],
escapeText: (str) =>
str
.replace(/\r/g, "")
.replace(/\n/g, "^\n\n")
.replace(/\%/g, "%%")
.replace(/\|/g, "^|")
.replace(/\(/g, "^(")
.replace(/\)/g, "^)"),
}),
...cmdEnv.wincmd,
...cmdCall.all,
...cmdExpectCommandOutput.wincmd,
...redirectOutput.bash,
}

12
e2e/shellcode/shells/cmdCall.ts

@ -0,0 +1,12 @@
import { define, ScriptLine } from "./types.js"
export type HasCall = {
call: (binary: string, args: string[]) => ScriptLine
}
export const cmdCall = {
all: define<HasCall>({
call: (binary: string, args: string[]) =>
`${binary} ${args.join(" ")}` as ScriptLine,
}),
}

33
e2e/shellcode/shells/cmdEnv.ts

@ -0,0 +1,33 @@
import { ScriptLine, define } from "./types.js"
type EnvConfig = { useOnCd: boolean; logLevel: string; args: string[] }
export type HasEnv = { env(cfg: Partial<EnvConfig>): ScriptLine }
function stringify(envConfig: Partial<EnvConfig> = {}) {
const { useOnCd, logLevel, args } = envConfig
return [
`fnm env`,
useOnCd && "--use-on-cd",
logLevel && `--log-level=${logLevel}`,
args && args.join(" "),
]
.filter(Boolean)
.join(" ")
}
export const cmdEnv = {
bash: define<HasEnv>({
env: (envConfig) => `eval "$(${stringify(envConfig)})"`,
}),
fish: define<HasEnv>({
env: (envConfig) => `${stringify(envConfig)} | source`,
}),
powershell: define<HasEnv>({
env: (envConfig) =>
`${stringify(envConfig)} | Out-String | Invoke-Expression`,
}),
wincmd: define<HasEnv>({
env: (envConfig) =>
`FOR /f "tokens=*" %i IN ('${stringify(envConfig)}') DO CALL %i`,
}),
}

52
e2e/shellcode/shells/expect-command-output.ts

@ -0,0 +1,52 @@
import { dedent } from "ts-dedent"
import { define, ScriptLine } from "./types.js"
export type HasExpectCommandOutput = {
hasCommandOutput(
script: ScriptLine,
output: string,
message: string
): ScriptLine
}
export const cmdExpectCommandOutput = {
bash: define<HasExpectCommandOutput>({
hasCommandOutput(script, output, message) {
return dedent`
if [ "$(${script})" != "${output}" ]; then
echo "Expected ${message} to be ${output}. Got $(${script})"
exit 1
fi
`
},
}),
fish: define<HasExpectCommandOutput>({
hasCommandOutput(script, output, message) {
return dedent`
set ____test____ (${script})
if test "$____test____" != "${output}"
echo "Expected ${message} to be ${output}. Got $____test____"
exit 1
end
`
},
}),
powershell: define<HasExpectCommandOutput>({
hasCommandOutput(script, output, message) {
return dedent`
if ( "$(${script})" -ne "${output}" ) { echo "Expected ${message} to be ${output}. Got $(${script})"; exit 1 }
`
},
}),
wincmd: define<HasExpectCommandOutput>({
hasCommandOutput(script, output, message) {
return dedent`
${script} | findstr ${output}
if %errorlevel% neq 0 (
echo Expected ${message} to be ${output}
exit 1
)
`
},
}),
}

24
e2e/shellcode/shells/output-contains.ts

@ -0,0 +1,24 @@
import { define, ScriptLine } from "./types.js"
export type HasOutputContains = {
scriptOutputContains(script: ScriptLine, substring: string): ScriptLine
}
export const cmdHasOutputContains = {
bash: define<HasOutputContains>({
scriptOutputContains: (script, substring) => {
return `(${script}) | grep ${substring} || (echo "Expected output to contain ${substring}" && exit 1)`
},
}),
fish: define<HasOutputContains>({
scriptOutputContains: (script, substring) => {
return `begin; ${script}; end | grep ${substring}; or echo "Expected output to contain ${substring}" && exit 1`
},
}),
powershell: define<HasOutputContains>({
scriptOutputContains: (script, substring) => {
const inner: string = `${script} | Select-String ${substring}`
return `$($__out__ = $(${inner}); if ($__out__ -eq $null) { exit 1 } else { $__out__ })`
},
}),
}

16
e2e/shellcode/shells/redirect-output.ts

@ -0,0 +1,16 @@
import { ScriptLine, define } from "./types.js"
type RedirectOutputOpts = { output: string }
export type HasRedirectOutput = {
redirectOutput(childCommand: ScriptLine, opts: RedirectOutputOpts): string
}
export const redirectOutput = {
bash: define<HasRedirectOutput>({
redirectOutput: (childCommand, opts) => `${childCommand} > ${opts.output}`,
}),
powershell: define<HasRedirectOutput>({
redirectOutput: (childCommand, opts) =>
`${childCommand} | Out-File ${opts.output} -Encoding UTF8`,
}),
}

20
e2e/shellcode/shells/sub-shell.ts

@ -0,0 +1,20 @@
import { ScriptLine, define } from "./types.js"
import quote from "shell-escape"
type HasInSubShell = { inSubShell: (script: ScriptLine) => ScriptLine }
export const cmdInSubShell = {
bash: define<HasInSubShell>({
inSubShell: (script) => `echo ${quote([script])} | bash`,
}),
zsh: define<HasInSubShell>({
inSubShell: (script) => `echo ${quote([script])} | zsh`,
}),
fish: define<HasInSubShell>({
inSubShell: (script) => `fish -c ${quote([script])}`,
}),
powershell: define<HasInSubShell>({
inSubShell: (script) =>
`echo '${script.replace(/'/g, "\\'")}' | pwsh -NoProfile`,
}),
}

15
e2e/shellcode/shells/types.ts

@ -0,0 +1,15 @@
export type Shell = {
escapeText(str: string): string
binaryName(): string
currentlySupported(): boolean
name(): string
launchArgs(): string[]
dieOnErrors?(): string
forceFile?: true | string
}
export type ScriptLine = string
export function define<S>(s: S): S {
return s
}

9
e2e/shellcode/test-bin-dir.ts

@ -0,0 +1,9 @@
import { mkdirSync } from "node:fs"
import path from "node:path"
import testTmpDir from "./test-tmp-dir.js"
export default function testBinDir() {
const dir = path.join(testTmpDir(), "bin")
mkdirSync(dir, { recursive: true })
return dir
}

9
e2e/shellcode/test-cwd.ts

@ -0,0 +1,9 @@
import { mkdirSync } from "node:fs"
import path from "node:path"
import testTmpDir from "./test-tmp-dir.js"
export default function testCwd() {
const dir = path.join(testTmpDir(), "cwd")
mkdirSync(dir, { recursive: true })
return dir
}

10
e2e/shellcode/test-node-version.ts

@ -0,0 +1,10 @@
import { HasCall } from "./shells/cmdCall.js"
import { ScriptLine } from "./shells/types.js"
import { HasExpectCommandOutput } from "./shells/expect-command-output.js"
export default function testNodeVersion<
S extends HasCall & HasExpectCommandOutput
>(shell: S, version: string): ScriptLine {
const nodeVersion = shell.call("node", ["--version"])
return shell.hasCommandOutput(nodeVersion, version, "node version")
}

15
e2e/shellcode/test-tmp-dir.ts

@ -0,0 +1,15 @@
import { mkdirSync, rmSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
export default function testTmpDir(): string {
const testName = (expect.getState().currentTestName ?? "unknown")
.toLowerCase()
.replace(/[^a-z0-9]/gi, "_")
.replace(/_+/g, "_")
const tmpDir = join(tmpdir(), `shellcode/${testName}`)
mkdirSync(tmpDir, { recursive: true })
rmSync(join(tmpDir, "fnm/aliases"), { recursive: true, force: true })
return tmpDir
}

49
e2e/shims.test.ts

@ -0,0 +1,49 @@
import { readFile } from "node:fs/promises"
import { join } from "node:path"
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import testCwd from "./shellcode/test-cwd.js"
import describe from "./describe.js"
import testNodeVersion from "./shellcode/test-node-version.js"
for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
describe(shell, () => {
test(`outputs json`, async () => {
const filename = `file.json`
await script(shell)
.then(
shell.redirectOutput(
shell.call("fnm", ["env", "--json", "--with-shims"]),
{
output: filename,
}
)
)
.takeSnapshot(shell)
.execute(shell)
if (shell.currentlySupported()) {
const file = await readFile(join(testCwd(), filename), "utf8")
expect(JSON.parse(file)).toEqual({
FNM_ARCH: expect.any(String),
FNM_DIR: expect.any(String),
FNM_LOGLEVEL: "info",
FNM_MULTISHELL_PATH: expect.any(String),
FNM_NODE_DIST_MIRROR: expect.any(String),
FNM_VERSION_FILE_STRATEGY: "local",
FNM_VERSION_SWITCH_STRATEGY: "shims",
})
}
})
test(`runs Node through a shim`, async () => {
await script(shell)
.then(shell.env({ args: ["--with-shims"] }))
.then(shell.call("fnm", ["install", "12.0.0"]))
.then(shell.call("fnm", ["use", "12.0.0"]))
.then(testNodeVersion(shell, "v12.0.0"))
.takeSnapshot(shell)
.execute(shell)
})
})
}

38
e2e/system-node.test.ts

@ -0,0 +1,38 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import fs from "node:fs/promises"
import path from "node:path"
import describe from "./describe.js"
import testNodeVersion from "./shellcode/test-node-version.js"
import testBinDir from "./shellcode/test-bin-dir.js"
for (const shell of [Bash, Fish, PowerShell, WinCmd, Zsh]) {
describe(shell, () => {
// latest bash breaks this as it seems. gotta find a solution.
const t = process.platform === "darwin" && shell === Bash ? test.skip : test
t(`switches to system node`, async () => {
const customNode = path.join(testBinDir(), "node")
if (
process.platform === "win32" &&
[WinCmd, PowerShell].includes(shell)
) {
await fs.writeFile(customNode + ".cmd", "@echo custom")
} else {
await fs.writeFile(customNode, `#!/bin/bash\n\necho "custom"\n`)
// set executable
await fs.chmod(customNode, 0o766)
}
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "v10.10.0"]))
.then(shell.call("fnm", ["use", "v10"]))
.then(testNodeVersion(shell, "v10.10.0"))
.then(shell.call("fnm", ["use", "system"]))
.then(testNodeVersion(shell, "custom"))
.execute(shell)
})
})
}

29
e2e/uninstall.test.ts

@ -0,0 +1,29 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`uninstalls a version`, async () => {
await script(shell)
.then(shell.call("fnm", ["install", "12.0.0"]))
.then(shell.call("fnm", ["alias", "12.0.0", "hello"]))
.then(
shell.scriptOutputContains(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "v12.0.0"),
"hello"
)
)
.then(shell.call("fnm", ["uninstall", "hello"]))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["ls"]),
"* system",
"fnm ls"
)
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

22
jest.config.cjs

@ -0,0 +1,22 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest/presets/default-esm",
testEnvironment: "node",
testTimeout: 120000,
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
"#ansi-styles": "ansi-styles/index.js",
"#supports-color": "supports-color/index.js",
},
transform: {
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: true,
},
],
},
}

33
package.json

@ -1,13 +1,15 @@
{ {
"name": "fnm", "name": "fnm",
"version": "0.0.0", "version": "1.31.1",
"private": true, "private": true,
"repository": "git@github.com:Schniz/fnm.git", "repository": "git@github.com:Schniz/fnm.git",
"author": "Gal Schlezinger <gal@spitfire.co.il>", "author": "Gal Schlezinger <gal@spitfire.co.il>",
"type": "module",
"packageManager": "pnpm@7.16.1",
"license": "GPLv3", "license": "GPLv3",
"scripts": { "scripts": {
"changelog": "./.ci/generate-changelog.sh", "test": "cross-env NODE_OPTIONS='--experimental-vm-modules' jest",
"version:prepare": "./.ci/prepare-version.js", "version:prepare": "changeset version && ./.ci/prepare-version.js",
"generate-command-docs": "./.ci/print-command-docs.js" "generate-command-docs": "./.ci/print-command-docs.js"
}, },
"changelog": { "changelog": {
@ -20,10 +22,27 @@
} }
}, },
"devDependencies": { "devDependencies": {
"cmd-ts": "0.8.0", "@changesets/cli": "2.25.0",
"execa": "5.1.1", "@svitejs/changesets-changelog-github-compact": "0.1.1",
"@types/jest": "^29.2.3",
"@types/node": "^18.11.9",
"@types/shell-escape": "^0.2.1",
"chalk": "^5.1.2",
"cmd-ts": "0.11.0",
"cross-env": "^7.0.3",
"execa": "6.1.0",
"jest": "^29.3.1",
"lerna-changelog": "2.2.0", "lerna-changelog": "2.2.0",
"prettier": "2.5.1", "prettier": "2.7.1",
"toml": "3.0.0" "pv": "1.0.1",
"shell-escape": "^0.2.0",
"svg-term-cli": "2.1.1",
"toml": "3.0.0",
"ts-dedent": "^2.2.0",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4"
},
"prettier": {
"semi": false
} }
} }

5129
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

28
renovate.json

@ -1,4 +1,6 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"labels": ["PR: Dependency Update"], "labels": ["PR: Dependency Update"],
"packageRules": [ "packageRules": [
{ {
@ -7,9 +9,29 @@
"groupName": "all non-major dependencies", "groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch" "groupSlug": "all-minor-patch"
} }
},
{
"matchPackagePatterns": ["serde", "serde_json"],
"groupName": "serde",
"groupSlug": "serde"
},
{
"matchPackagePatterns": ["clap", "clap_*"],
"groupName": "clap-rs",
"groupSlug": "clap-rs"
},
{
"matchDepTypes": ["devDependencies", "dev-dependencies"],
"matchPackagePatterns": ["*"],
"automerge": true,
"groupName": "all dev dependencies",
"groupSlug": "all-dev-dependencies"
},
{
"matchPackagePatterns": ["uraimo/run-on-arch-action"],
"matchManagers": ["github-actions"],
"allowedVersions": "2.1.2"
} }
], ],
"extends": [ "lockFileMaintenance": { "enabled": true }
"config:base"
]
} }

5
site/package.json

@ -6,10 +6,5 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "rm -rf public; mkdir public; cp ../.ci/install.sh ./public/install.txt" "build": "rm -rf public; mkdir public; cp ../.ci/install.sh ./public/install.txt"
},
"devDependencies": {
"@vercel/node": "1.12.1",
"typescript": "4.5.4",
"vercel": "23.1.2"
} }
} }

10
site/tsconfig.json

@ -1,10 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

14
site/vercel.json

@ -1,17 +1,19 @@
{ {
"$schema": "https://openapi.vercel.sh/vercel.json",
"github": {
"silent": true
},
"redirects": [ "redirects": [
{ "source": "/", "destination": "https://github.com/Schniz/fnm" } { "source": "/", "destination": "https://github.com/Schniz/fnm" }
], ],
"rewrites": [ "rewrites": [{ "source": "/install", "destination": "/install.txt" }],
{ "source": "/install", "destination": "/install.txt" }
],
"headers": [ "headers": [
{ {
"source": "/install", "source": "/install",
"headers" : [ "headers": [
{ {
"key" : "Cache-Control", "key": "Cache-Control",
"value" : "public, max-age=3600" "value": "public, max-age=3600"
} }
] ]
} }

677
site/yarn.lock

@ -1,677 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
"@szmarczak/http-timer@^1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
dependencies:
defer-to-connect "^1.0.1"
"@types/color-name@^1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/node@*":
version "14.11.2"
resolved "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==
"@vercel/build-utils@2.12.2":
version "2.12.2"
resolved "https://registry.yarnpkg.com/@vercel/build-utils/-/build-utils-2.12.2.tgz#285a3bb0b78864fb6f44478257bd275c57aa8651"
integrity sha512-KbSgG2ZCVXhUsdbnpv6gC7buygd31jaKiKhrd4Lzv1NwjnoeDZAXlm4hzvSPYHVtCY2jirKJWP2rFtMW8iAh9g==
"@vercel/go@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@vercel/go/-/go-1.2.3.tgz#6f2bdba5681f9d64ee17060c5d63589e2d45e2d8"
integrity sha512-BZCHRz43Qfr0DwZlZQCcofR+3cr+H+HK72/ZPkZy1Uq0NYjJMlmZ3ahuMgvJxT9lfC1RA6eOEUlUsZ+gqKcMCg==
"@vercel/node@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@vercel/node/-/node-1.12.1.tgz#15f42f64690f904f8a52a387123ce0958657060f"
integrity sha512-NcawIY05BvVkWlsowaxF2hl/hJg475U8JvT2FnGykFPMx31q1/FtqyTw/awSrKfOSRXR0InrbEIDIelmS9NzPA==
dependencies:
"@types/node" "*"
ts-node "8.9.1"
typescript "4.3.4"
"@vercel/python@2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@vercel/python/-/python-2.0.5.tgz#76c09280febfac863c39651edffafbb0838a1df8"
integrity sha512-WCSTTw6He2COaSBiGDk2q5Q1ue+z5usRZcvUHCpsK6KvNkkV/PrY8JT73XQysMWKiXh6yQy19IUFAOqK/xwhig==
"@vercel/ruby@1.2.7":
version "1.2.7"
resolved "https://registry.yarnpkg.com/@vercel/ruby/-/ruby-1.2.7.tgz#516d1c45f4961619338f3d3bb518ee43b863a5da"
integrity sha512-ZG2VxMHHSKocL57UWsfNc9UsblwYGm55/ujqGIBnkNUURnRgtUrwtWlEts1eJ4VHD754Lc/0/R1pfJXoN5SbRw==
ansi-align@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb"
integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==
dependencies:
string-width "^3.0.0"
ansi-regex@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
ansi-regex@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
ansi-styles@^4.1.0:
version "4.2.1"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
dependencies:
"@types/color-name" "^1.1.1"
color-convert "^2.0.1"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
boxen@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64"
integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==
dependencies:
ansi-align "^3.0.0"
camelcase "^5.3.1"
chalk "^3.0.0"
cli-boxes "^2.2.0"
string-width "^4.1.0"
term-size "^2.1.0"
type-fest "^0.8.1"
widest-line "^3.1.0"
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
cacheable-request@^6.0.0:
version "6.1.0"
resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==
dependencies:
clone-response "^1.0.2"
get-stream "^5.1.0"
http-cache-semantics "^4.0.0"
keyv "^3.0.0"
lowercase-keys "^2.0.0"
normalize-url "^4.1.0"
responselike "^1.0.2"
camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
cli-boxes@^2.2.0:
version "2.2.1"
resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
clone-response@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
dependencies:
mimic-response "^1.0.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
configstore@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==
dependencies:
dot-prop "^5.2.0"
graceful-fs "^4.1.2"
make-dir "^3.0.0"
unique-string "^2.0.0"
write-file-atomic "^3.0.0"
xdg-basedir "^4.0.0"
crypto-random-string@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
decompress-response@^3.3.0:
version "3.3.0"
resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
dependencies:
mimic-response "^1.0.0"
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
defer-to-connect@^1.0.1:
version "1.1.3"
resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
dot-prop@^5.2.0:
version "5.3.0"
resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==
dependencies:
is-obj "^2.0.0"
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
emoji-regex@^7.0.1:
version "7.0.3"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
dependencies:
once "^1.4.0"
escape-goat@^2.0.0:
version "2.1.1"
resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
get-stream@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
dependencies:
pump "^3.0.0"
get-stream@^5.1.0:
version "5.2.0"
resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
dependencies:
pump "^3.0.0"
global-dirs@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201"
integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==
dependencies:
ini "^1.3.5"
got@^9.6.0:
version "9.6.0"
resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
dependencies:
"@sindresorhus/is" "^0.14.0"
"@szmarczak/http-timer" "^1.1.2"
cacheable-request "^6.0.0"
decompress-response "^3.3.0"
duplexer3 "^0.1.4"
get-stream "^4.1.0"
lowercase-keys "^1.0.1"
mimic-response "^1.0.1"
p-cancelable "^1.0.0"
to-readable-stream "^1.0.0"
url-parse-lax "^3.0.0"
graceful-fs@^4.1.2:
version "4.2.4"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-yarn@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77"
integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==
http-cache-semantics@^4.0.0:
version "4.1.0"
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
import-lazy@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
ini@^1.3.5, ini@~1.3.0:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
dependencies:
ci-info "^2.0.0"
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-installed-globally@^0.3.1:
version "0.3.2"
resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141"
integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==
dependencies:
global-dirs "^2.0.1"
is-path-inside "^3.0.1"
is-npm@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==
is-obj@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
is-path-inside@^3.0.1:
version "3.0.2"
resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017"
integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==
is-typedarray@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-yarn-global@^0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
json-buffer@3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
keyv@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==
dependencies:
json-buffer "3.0.0"
latest-version@^5.0.0:
version "5.1.0"
resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face"
integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==
dependencies:
package-json "^6.3.0"
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
lowercase-keys@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
make-dir@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
dependencies:
semver "^6.0.0"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
mimic-response@^1.0.0, mimic-response@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
minimist@^1.2.0:
version "1.2.5"
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
normalize-url@^4.1.0:
version "4.5.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
p-cancelable@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
package-json@^6.3.0:
version "6.5.0"
resolved "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==
dependencies:
got "^9.6.0"
registry-auth-token "^4.0.0"
registry-url "^5.0.0"
semver "^6.2.0"
prepend-http@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
pupa@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726"
integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==
dependencies:
escape-goat "^2.0.0"
rc@^1.2.8:
version "1.2.8"
resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
dependencies:
deep-extend "^0.6.0"
ini "~1.3.0"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
registry-auth-token@^4.0.0:
version "4.2.0"
resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da"
integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==
dependencies:
rc "^1.2.8"
registry-url@^5.0.0:
version "5.1.0"
resolved "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009"
integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==
dependencies:
rc "^1.2.8"
responselike@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
dependencies:
lowercase-keys "^1.0.0"
semver-diff@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==
dependencies:
semver "^6.3.0"
semver@^6.0.0, semver@^6.2.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
source-map-support@^0.5.17:
version "0.5.19"
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
string-width@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
dependencies:
emoji-regex "^7.0.1"
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
string-width@^4.0.0, string-width@^4.1.0:
version "4.2.0"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
strip-ansi@^5.1.0:
version "5.2.0"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
dependencies:
ansi-regex "^5.0.0"
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
term-size@^2.1.0:
version "2.2.0"
resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753"
integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==
to-readable-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
ts-node@8.9.1:
version "8.9.1"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.9.1.tgz#2f857f46c47e91dcd28a14e052482eb14cfd65a5"
integrity sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==
dependencies:
arg "^4.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.17"
yn "3.1.1"
type-fest@^0.8.1:
version "0.8.1"
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
dependencies:
is-typedarray "^1.0.0"
typescript@4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc"
integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==
typescript@4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
unique-string@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==
dependencies:
crypto-random-string "^2.0.0"
update-notifier@4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz#4866b98c3bc5b5473c020b1250583628f9a328f3"
integrity sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==
dependencies:
boxen "^4.2.0"
chalk "^3.0.0"
configstore "^5.0.1"
has-yarn "^2.1.0"
import-lazy "^2.1.0"
is-ci "^2.0.0"
is-installed-globally "^0.3.1"
is-npm "^4.0.0"
is-yarn-global "^0.3.0"
latest-version "^5.0.0"
pupa "^2.0.1"
semver-diff "^3.1.1"
xdg-basedir "^4.0.0"
url-parse-lax@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=
dependencies:
prepend-http "^2.0.0"
vercel@23.1.2:
version "23.1.2"
resolved "https://registry.yarnpkg.com/vercel/-/vercel-23.1.2.tgz#7f36772970c7c56f10de89983f03b3c0c72d294e"
integrity sha512-uS1k7wuXI6hbxiW+kn9vdAWL0bBi4jjVxc7Jwp8NhJjcRuzlydtt3gUEnhnC9AOIKQ4LxoAgmg50lSyYkrC8Hg==
dependencies:
"@vercel/build-utils" "2.12.2"
"@vercel/go" "1.2.3"
"@vercel/node" "1.12.1"
"@vercel/python" "2.0.5"
"@vercel/ruby" "1.2.7"
update-notifier "4.1.0"
widest-line@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==
dependencies:
string-width "^4.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
write-file-atomic@^3.0.0:
version "3.0.3"
resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
dependencies:
imurmurhash "^0.1.4"
is-typedarray "^1.0.0"
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"
xdg-basedir@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==

26
src/arch.rs

@ -11,6 +11,20 @@ pub enum Arch {
S390x, S390x,
} }
impl Arch {
pub fn as_str(&self) -> &'static str {
match self {
Arch::X86 => "x86",
Arch::X64 => "x64",
Arch::Arm64 => "arm64",
Arch::Armv7l => "armv7l",
Arch::Ppc64le => "ppc64le",
Arch::Ppc64 => "ppc64",
Arch::S390x => "s390x",
}
}
}
#[cfg(unix)] #[cfg(unix)]
/// handle common case: Apple Silicon / Node < 16 /// handle common case: Apple Silicon / Node < 16
pub fn get_safe_arch<'a>(arch: &'a Arch, version: &Version) -> &'a Arch { pub fn get_safe_arch<'a>(arch: &'a Arch, version: &Version) -> &'a Arch {
@ -55,17 +69,7 @@ impl std::str::FromStr for Arch {
impl std::fmt::Display for Arch { impl std::fmt::Display for Arch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let arch_str = match self { write!(f, "{}", self.as_str())
Arch::X86 => String::from("x86"),
Arch::X64 => String::from("x64"),
Arch::Arm64 => String::from("arm64"),
Arch::Armv7l => String::from("armv7l"),
Arch::Ppc64le => String::from("ppc64le"),
Arch::Ppc64 => String::from("ppc64"),
Arch::S390x => String::from("s390x"),
};
write!(f, "{}", arch_str)
} }
} }

4
src/archive/zip.rs

@ -42,7 +42,7 @@ impl<R: Read> Extract for Zip<R> {
} }
} }
if (&*file.name()).ends_with('/') { if file.name().ends_with('/') {
debug!( debug!(
"File {} extracted to \"{}\"", "File {} extracted to \"{}\"",
i, i,
@ -58,7 +58,7 @@ impl<R: Read> Extract for Zip<R> {
); );
if let Some(p) = outpath.parent() { if let Some(p) = outpath.parent() {
if !p.exists() { if !p.exists() {
fs::create_dir_all(&p)?; fs::create_dir_all(p)?;
} }
} }
let mut outfile = fs::File::create(&outpath)?; let mut outfile = fs::File::create(&outpath)?;

24
src/choose_version_for_user_input.rs

@ -6,8 +6,8 @@ use crate::user_version::UserVersion;
use crate::version::Version; use crate::version::Version;
use colored::Colorize; use colored::Colorize;
use log::info; use log::info;
use snafu::{ensure, ResultExt, Snafu};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug)] #[derive(Debug)]
pub struct ApplicableVersion { pub struct ApplicableVersion {
@ -29,8 +29,8 @@ pub fn choose_version_for_user_input<'a>(
requested_version: &'a UserVersion, requested_version: &'a UserVersion,
config: &'a FnmConfig, config: &'a FnmConfig,
) -> Result<Option<ApplicableVersion>, Error> { ) -> Result<Option<ApplicableVersion>, Error> {
let all_versions = let all_versions = installed_versions::list(config.installations_dir())
installed_versions::list(config.installations_dir()).context(VersionListing)?; .map_err(|source| Error::VersionListing { source })?;
let result = if let UserVersion::Full(Version::Bypassed) = requested_version { let result = if let UserVersion::Full(Version::Bypassed) = requested_version {
info!( info!(
@ -54,18 +54,16 @@ pub fn choose_version_for_user_input<'a>(
path: alias_path, path: alias_path,
version: Version::Bypassed, version: Version::Bypassed,
}) })
} else { } else if alias_path.exists() {
ensure!(
alias_path.exists(),
CantFindVersion {
requested_version: requested_version.clone()
}
);
info!("Using Node for alias {}", alias_name.cyan()); info!("Using Node for alias {}", alias_name.cyan());
Some(ApplicableVersion { Some(ApplicableVersion {
path: alias_path, path: alias_path,
version: Version::Alias(alias_name), version: Version::Alias(alias_name),
}) })
} else {
return Err(Error::CantFindVersion {
requested_version: requested_version.clone(),
});
} }
} else { } else {
let current_version = requested_version.to_version(&all_versions, config); let current_version = requested_version.to_version(&all_versions, config);
@ -86,10 +84,10 @@ pub fn choose_version_for_user_input<'a>(
Ok(result) Ok(result)
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[snafu(display("Can't find requested version: {}", requested_version))] #[error("Can't find requested version: {}", requested_version)]
CantFindVersion { requested_version: UserVersion }, CantFindVersion { requested_version: UserVersion },
#[snafu(display("Can't list local installed versions: {}", source))] #[error("Can't list local installed versions: {}", source)]
VersionListing { source: installed_versions::Error }, VersionListing { source: installed_versions::Error },
} }

38
src/cli.rs

@ -1,24 +1,24 @@
use crate::commands; use crate::commands;
use crate::commands::command::Command; use crate::commands::command::Command;
use crate::config::FnmConfig; use crate::config::FnmConfig;
use structopt::StructOpt; use clap::Parser;
#[derive(StructOpt, Debug)] #[derive(clap::Parser, Debug)]
pub enum SubCommand { pub enum SubCommand {
/// List all remote Node.js versions /// List all remote Node.js versions
#[structopt(name = "list-remote", visible_aliases = &["ls-remote"])] #[clap(name = "list-remote", bin_name = "list-remote", visible_aliases = &["ls-remote"])]
LsRemote(commands::ls_remote::LsRemote), LsRemote(commands::ls_remote::LsRemote),
/// List all locally installed Node.js versions /// List all locally installed Node.js versions
#[structopt(name = "list", visible_aliases = &["ls"])] #[clap(name = "list", bin_name = "list", visible_aliases = &["ls"])]
LsLocal(commands::ls_local::LsLocal), LsLocal(commands::ls_local::LsLocal),
/// Install a new Node.js version /// Install a new Node.js version
#[structopt(name = "install")] #[clap(name = "install", bin_name = "install")]
Install(commands::install::Install), Install(commands::install::Install),
/// Change Node.js version /// Change Node.js version
#[structopt(name = "use")] #[clap(name = "use", bin_name = "use")]
Use(commands::r#use::Use), Use(commands::r#use::Use),
/// Print and set up required environment variables for fnm /// Print and set up required environment variables for fnm
@ -29,29 +29,29 @@ pub enum SubCommand {
/// Each shell has its own syntax of evaluating a dynamic expression. /// Each shell has its own syntax of evaluating a dynamic expression.
/// For example, evaluating fnm on Bash and Zsh would look like `eval "$(fnm env)"`. /// For example, evaluating fnm on Bash and Zsh would look like `eval "$(fnm env)"`.
/// In Fish, evaluating would look like `fnm env | source` /// In Fish, evaluating would look like `fnm env | source`
#[structopt(name = "env")] #[clap(name = "env", bin_name = "env")]
Env(commands::env::Env), Env(commands::env::Env),
/// Print shell completions to stdout /// Print shell completions to stdout
#[structopt(name = "completions")] #[clap(name = "completions", bin_name = "completions")]
Completions(commands::completions::Completions), Completions(commands::completions::Completions),
/// Alias a version to a common name /// Alias a version to a common name
#[structopt(name = "alias")] #[clap(name = "alias", bin_name = "alias")]
Alias(commands::alias::Alias), Alias(commands::alias::Alias),
/// Remove an alias definition /// Remove an alias definition
#[structopt(name = "unalias")] #[clap(name = "unalias", bin_name = "unalias")]
Unalias(commands::unalias::Unalias), Unalias(commands::unalias::Unalias),
/// Set a version as the default version /// Set a version as the default version
/// ///
/// This is a shorthand for `fnm alias VERSION default` /// This is a shorthand for `fnm alias VERSION default`
#[structopt(name = "default")] #[clap(name = "default", bin_name = "default")]
Default(commands::default::Default), Default(commands::default::Default),
/// Print the current Node.js version /// Print the current Node.js version
#[structopt(name = "current")] #[clap(name = "current", bin_name = "current")]
Current(commands::current::Current), Current(commands::current::Current),
/// Run a command within fnm context /// Run a command within fnm context
@ -60,14 +60,14 @@ pub enum SubCommand {
/// -------- /// --------
/// fnm exec --using=v12.0.0 node --version /// fnm exec --using=v12.0.0 node --version
/// => v12.0.0 /// => v12.0.0
#[structopt(name = "exec", verbatim_doc_comment)] #[clap(name = "exec", bin_name = "exec", verbatim_doc_comment)]
Exec(commands::exec::Exec), Exec(commands::exec::Exec),
/// Uninstall a Node.js version /// Uninstall a Node.js version
/// ///
/// > Warning: when providing an alias, it will remove the Node version the alias /// > Warning: when providing an alias, it will remove the Node version the alias
/// is pointing to, along with the other aliases that point to the same version. /// is pointing to, along with the other aliases that point to the same version.
#[structopt(name = "uninstall")] #[clap(name = "uninstall", bin_name = "uninstall")]
Uninstall(commands::uninstall::Uninstall), Uninstall(commands::uninstall::Uninstall),
} }
@ -91,15 +91,15 @@ impl SubCommand {
} }
/// A fast and simple Node.js manager. /// A fast and simple Node.js manager.
#[derive(StructOpt, Debug)] #[derive(clap::Parser, Debug)]
#[structopt(name = "fnm")] #[clap(name = "fnm", version = env!("CARGO_PKG_VERSION"), bin_name = "fnm")]
pub struct Cli { pub struct Cli {
#[structopt(flatten)] #[clap(flatten)]
pub config: FnmConfig, pub config: FnmConfig,
#[structopt(subcommand)] #[clap(subcommand)]
pub subcmd: SubCommand, pub subcmd: SubCommand,
} }
pub fn parse() -> Cli { pub fn parse() -> Cli {
Cli::from_args() Cli::parse()
} }

22
src/commands/alias.rs

@ -4,12 +4,10 @@ use crate::choose_version_for_user_input::{
choose_version_for_user_input, Error as ApplicableVersionError, choose_version_for_user_input, Error as ApplicableVersionError,
}; };
use crate::config::FnmConfig; use crate::config::FnmConfig;
use crate::installed_versions;
use crate::user_version::UserVersion; use crate::user_version::UserVersion;
use snafu::{OptionExt, ResultExt, Snafu}; use thiserror::Error;
use structopt::StructOpt;
#[derive(StructOpt, Debug)] #[derive(clap::Parser, Debug)]
pub struct Alias { pub struct Alias {
pub(crate) to_version: UserVersion, pub(crate) to_version: UserVersion,
pub(crate) name: String, pub(crate) name: String,
@ -20,26 +18,24 @@ impl Command for Alias {
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> { fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let applicable_version = choose_version_for_user_input(&self.to_version, config) let applicable_version = choose_version_for_user_input(&self.to_version, config)
.context(CantUnderstandVersion)? .map_err(|source| Error::CantUnderstandVersion { source })?
.context(VersionNotFound { .ok_or(Error::VersionNotFound {
version: self.to_version, version: self.to_version,
})?; })?;
create_alias(config, &self.name, applicable_version.version()) create_alias(config, &self.name, applicable_version.version())
.context(CantCreateSymlink)?; .map_err(|source| Error::CantCreateSymlink { source })?;
Ok(()) Ok(())
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[snafu(display("Can't create symlink for alias: {}", source))] #[error("Can't create symlink for alias: {}", source)]
CantCreateSymlink { source: std::io::Error }, CantCreateSymlink { source: std::io::Error },
#[snafu(display("Can't list local installed versions: {}", source))] #[error("Version {} not found locally", version)]
VersionListingError { source: installed_versions::Error },
#[snafu(display("Version {} not found locally", version))]
VersionNotFound { version: UserVersion }, VersionNotFound { version: UserVersion },
#[snafu(display("{}", source))] #[error(transparent)]
CantUnderstandVersion { source: ApplicableVersionError }, CantUnderstandVersion { source: ApplicableVersionError },
} }

21
src/commands/completions.rs

@ -2,14 +2,14 @@ use super::command::Command;
use crate::cli::Cli; use crate::cli::Cli;
use crate::config::FnmConfig; use crate::config::FnmConfig;
use crate::shell::{infer_shell, AVAILABLE_SHELLS}; use crate::shell::{infer_shell, AVAILABLE_SHELLS};
use snafu::{OptionExt, Snafu}; use clap::{IntoApp, Parser};
use structopt::clap::Shell; use clap_complete::{Generator, Shell};
use structopt::StructOpt; use thiserror::Error;
#[derive(StructOpt, Debug)] #[derive(Parser, Debug)]
pub struct Completions { pub struct Completions {
/// The shell syntax to use. Infers when missing. /// The shell syntax to use. Infers when missing.
#[structopt(long, possible_values = &Shell::variants())] #[clap(long)]
shell: Option<Shell>, shell: Option<Shell>,
} }
@ -21,21 +21,22 @@ impl Command for Completions {
let shell = self let shell = self
.shell .shell
.or_else(|| infer_shell().map(Into::into)) .or_else(|| infer_shell().map(Into::into))
.context(CantInferShell)?; .ok_or(Error::CantInferShell)?;
Cli::clap().gen_completions_to(env!("CARGO_PKG_NAME"), shell, &mut stdio); let app = Cli::command();
shell.generate(&app, &mut stdio);
Ok(()) Ok(())
} }
} }
#[derive(Snafu, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[snafu(display( #[error(
"{}\n{}\n{}\n{}", "{}\n{}\n{}\n{}",
"Can't infer shell!", "Can't infer shell!",
"fnm can't infer your shell based on the process tree.", "fnm can't infer your shell based on the process tree.",
"Maybe it is unsupported? we support the following shells:", "Maybe it is unsupported? we support the following shells:",
shells_as_string() shells_as_string()
))] )]
CantInferShell, CantInferShell,
} }

3
src/commands/current.rs

@ -1,9 +1,8 @@
use super::command::Command; use super::command::Command;
use crate::config::FnmConfig; use crate::config::FnmConfig;
use crate::current_version::{current_version, Error}; use crate::current_version::{current_version, Error};
use structopt::StructOpt;
#[derive(StructOpt, Debug)] #[derive(clap::Parser, Debug)]
pub struct Current {} pub struct Current {}
impl Command for Current { impl Command for Current {

3
src/commands/default.rs

@ -2,9 +2,8 @@ use super::alias::Alias;
use super::command::Command; use super::command::Command;
use crate::config::FnmConfig; use crate::config::FnmConfig;
use crate::user_version::UserVersion; use crate::user_version::UserVersion;
use structopt::StructOpt;
#[derive(StructOpt, Debug)] #[derive(clap::Parser, Debug)]
pub struct Default { pub struct Default {
version: UserVersion, version: UserVersion,
} }

124
src/commands/env.rs

@ -1,25 +1,30 @@
use super::command::Command; use super::command::Command;
use crate::config::FnmConfig; use crate::config::FnmConfig;
use crate::directories;
use crate::fs::symlink_dir; use crate::fs::symlink_dir;
use crate::path_ext::PathExt; use crate::path_ext::PathExt;
use crate::shell::{infer_shell, Shell, AVAILABLE_SHELLS}; use crate::shell::{infer_shell, Shell, AVAILABLE_SHELLS};
use crate::version_switch_strategy::VersionSwitchStrategy;
use crate::{outln, shims}; use crate::{outln, shims};
use colored::Colorize; use colored::Colorize;
use snafu::{OptionExt, ResultExt, Snafu}; use std::collections::HashMap;
use std::fmt::Debug; use std::fmt::Debug;
use structopt::StructOpt; use thiserror::Error;
#[derive(StructOpt, Debug, Default)] #[derive(clap::Parser, Debug, Default)]
pub struct Env { pub struct Env {
/// The shell syntax to use. Infers when missing. /// The shell syntax to use. Infers when missing.
#[structopt(long)] #[clap(long)]
#[structopt(possible_values = AVAILABLE_SHELLS)] #[clap(possible_values = AVAILABLE_SHELLS)]
shell: Option<Box<dyn Shell>>, shell: Option<Box<dyn Shell>>,
/// Print JSON instead of shell commands.
#[clap(long, conflicts_with = "shell")]
json: bool,
/// Deprecated. This is the default now. /// Deprecated. This is the default now.
#[structopt(long, hidden = true)] #[clap(long, hide = true)]
multi: bool, multi: bool,
/// Print the script to change Node versions every directory change /// Print the script to change Node versions every directory change
#[structopt(long)] #[clap(long)]
use_on_cd: bool, use_on_cd: bool,
/// Adds `node` shims to your PATH environment variable /// Adds `node` shims to your PATH environment variable
@ -37,18 +42,18 @@ fn generate_symlink_path() -> String {
) )
} }
fn make_symlink(config: &FnmConfig) -> std::path::PathBuf { fn make_symlink(config: &FnmConfig) -> Result<std::path::PathBuf, Error> {
let base_dir = std::env::temp_dir() let base_dir = directories::multishell_storage().ensure_exists_silently();
.join("fnm_multishells")
.ensure_exists_silently();
let mut temp_dir = base_dir.join(generate_symlink_path()); let mut temp_dir = base_dir.join(generate_symlink_path());
while temp_dir.exists() { while temp_dir.exists() {
temp_dir = base_dir.join(generate_symlink_path()); temp_dir = base_dir.join(generate_symlink_path());
} }
symlink_dir(config.default_version_dir(), &temp_dir).expect("Can't create symlink!"); match symlink_dir(config.default_version_dir(), &temp_dir) {
temp_dir Ok(_) => Ok(temp_dir),
Err(source) => Err(Error::CantCreateSymlink { source, temp_dir }),
}
} }
impl Command for Env { impl Command for Env {
@ -65,58 +70,91 @@ impl Command for Env {
); );
} }
let shell: Box<dyn Shell> = self.shell.or_else(&infer_shell).context(CantInferShell)?; let multishell_path = make_symlink(config)?;
let multishell_path = make_symlink(config);
let binary_path = if self.with_shims { let binary_path = if self.with_shims {
shims::store_shim(config).context(CantCreateShims)? shims::store_shim(config).map_err(|source| Error::CantCreateShims { source })?
} else if cfg!(windows) { } else if cfg!(windows) {
multishell_path.clone() multishell_path.clone()
} else { } else {
multishell_path.join("bin") multishell_path.join("bin")
}; };
println!("{}", shell.path(&binary_path));
println!( let fnm_dir = config.base_dir_with_default();
"{}",
shell.set_env_var("FNM_MULTISHELL_PATH", multishell_path.to_str().unwrap()) let env_vars = HashMap::from([
); ("FNM_MULTISHELL_PATH", multishell_path.to_str().unwrap()),
println!( (
"{}", "FNM_VERSION_FILE_STRATEGY",
shell.set_env_var("FNM_DIR", config.base_dir_with_default().to_str().unwrap()) config.version_file_strategy().as_str(),
); ),
println!( ("FNM_DIR", fnm_dir.to_str().unwrap()),
"{}", ("FNM_LOGLEVEL", config.log_level().as_str()),
shell.set_env_var("FNM_LOGLEVEL", config.log_level().clone().into()) ("FNM_NODE_DIST_MIRROR", config.node_dist_mirror.as_str()),
); ("FNM_ARCH", config.arch.as_str()),
println!( (
"{}", "FNM_VERSION_SWITCH_STRATEGY",
shell.set_env_var("FNM_NODE_DIST_MIRROR", config.node_dist_mirror.as_str()) if self.with_shims {
); VersionSwitchStrategy::Shims
println!( } else {
"{}", VersionSwitchStrategy::PathSymlink
shell.set_env_var("FNM_ARCH", &config.arch.to_string()) }
); .as_str(),
),
]);
if self.json {
println!("{}", serde_json::to_string(&env_vars).unwrap());
return Ok(());
}
let shell: Box<dyn Shell> = self
.shell
.or_else(infer_shell)
.ok_or(Error::CantInferShell)?;
println!("{}", shell.path(&binary_path)?);
for (name, value) in &env_vars {
println!("{}", shell.set_env_var(name, value));
}
if self.use_on_cd { if self.use_on_cd {
println!("{}", shell.use_on_cd(config)); println!("{}", shell.use_on_cd(config)?);
} }
if let Some(v) = shell.rehash() { if let Some(v) = shell.rehash() {
println!("{}", v); println!("{}", v);
} }
Ok(()) Ok(())
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[snafu(display( #[error(
"{}\n{}\n{}\n{}", "{}\n{}\n{}\n{}",
"Can't infer shell!", "Can't infer shell!",
"fnm can't infer your shell based on the process tree.", "fnm can't infer your shell based on the process tree.",
"Maybe it is unsupported? we support the following shells:", "Maybe it is unsupported? we support the following shells:",
shells_as_string() shells_as_string()
))] )]
CantInferShell, CantInferShell,
#[snafu(display("Can't create Node.js shims: {}", source))] #[error("Can't create Node.js shims: {}", source)]
CantCreateShims { source: std::io::Error }, CantCreateShims {
#[source]
source: std::io::Error,
},
#[error("Can't create the symlink for multishells at {temp_dir:?}. Maybe there are some issues with permissions for the directory? {source}")]
CantCreateSymlink {
#[source]
source: std::io::Error,
temp_dir: std::path::PathBuf,
},
#[error(transparent)]
ShellError {
#[from]
source: anyhow::Error,
},
} }
fn shells_as_string() -> String { fn shells_as_string() -> String {

72
src/commands/exec.rs

@ -9,20 +9,19 @@ use crate::user_version::UserVersion;
use crate::user_version_reader::UserVersionReader; use crate::user_version_reader::UserVersionReader;
use colored::Colorize; use colored::Colorize;
use log::debug; use log::debug;
use snafu::{OptionExt, ResultExt, Snafu};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use structopt::StructOpt; use thiserror::Error;
#[derive(Debug, StructOpt)] #[derive(Debug, clap::Parser)]
#[structopt(setting = structopt::clap::AppSettings::TrailingVarArg)] #[clap(trailing_var_arg = true)]
pub struct Exec { pub struct Exec {
/// Either an explicit version, or a filename with the version written in it /// Either an explicit version, or a filename with the version written in it
#[structopt(long = "using")] #[clap(long = "using")]
version: Option<UserVersionReader>, version: Option<UserVersionReader>,
/// Deprecated. This is the default now. /// Deprecated. This is the default now.
#[structopt(long = "using-file", hidden = true)] #[clap(long = "using-file", hide = true)]
using_file: bool, using_file: bool,
#[structopt(long = "using-current", hidden = true, conflicts_with = "using")] #[structopt(long = "using-current", hidden = true, conflicts_with = "version")]
using_current: bool, using_current: bool,
/// The command to run /// The command to run
arguments: Vec<String>, arguments: Vec<String>,
@ -42,12 +41,15 @@ impl Cmd for Exec {
); );
} }
let (binary, arguments) = self.arguments.split_first().context(NoBinaryProvided)?; let (binary, arguments) = self
.arguments
.split_first()
.ok_or(Error::NoBinaryProvided)?;
let version = if self.using_current { let version = if self.using_current {
let version = current_version(config) let version = current_version(config)
.context(CantGetCurrentVersion)? .map_err(|source| Error::CantGetCurrentVersion { source })?
.context(NoCurrentVersion)?; .ok_or(Error::NoCurrentVersion)?;
UserVersion::Full(version) UserVersion::Full(version)
} else { } else {
self.version self.version
@ -56,13 +58,13 @@ impl Cmd for Exec {
let current_dir = std::env::current_dir().unwrap(); let current_dir = std::env::current_dir().unwrap();
UserVersionReader::Path(current_dir) UserVersionReader::Path(current_dir)
}) })
.into_user_version() .into_user_version(config)
.context(CantInferVersion)? .ok_or(Error::CantInferVersion)?
}; };
let applicable_version = choose_version_for_user_input(&version, config) let applicable_version = choose_version_for_user_input(&version, config)
.context(ApplicableVersionError)? .map_err(|source| Error::ApplicableVersionError { source })?
.context(VersionNotFound { version })?; .ok_or(Error::VersionNotFound { version })?;
#[cfg(windows)] #[cfg(windows)]
let bin_path = applicable_version.path().to_path_buf(); let bin_path = applicable_version.path().to_path_buf();
@ -73,13 +75,14 @@ impl Cmd for Exec {
debug!("Using Node.js from {}", bin_path.display()); debug!("Using Node.js from {}", bin_path.display());
let path_env = { let path_env = {
let paths_env = std::env::var_os("PATH").context(CantReadPathVariable)?; let paths_env = std::env::var_os("PATH").ok_or(Error::CantReadPathVariable)?;
let mut paths: Vec<_> = std::env::split_paths(&paths_env).collect(); let mut paths: Vec<_> = std::env::split_paths(&paths_env).collect();
paths.insert(0, bin_path); paths.insert(0, bin_path);
std::env::join_paths(paths).context(CantAddPathToEnvironment)? std::env::join_paths(paths)
.map_err(|source| Error::CantAddPathToEnvironment { source })?
}; };
let exit_status = Command::new(&binary) let exit_status = Command::new(binary)
.args(arguments) .args(arguments)
.stdin(Stdio::inherit()) .stdin(Stdio::inherit())
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
@ -90,40 +93,35 @@ impl Cmd for Exec {
.wait() .wait()
.expect("Failed to grab exit code"); .expect("Failed to grab exit code");
let code = exit_status.code().context(CantReadProcessExitCode)?; let code = exit_status.code().ok_or(Error::CantReadProcessExitCode)?;
std::process::exit(code); std::process::exit(code);
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[snafu(display("Can't read path environment variable"))] #[error("Can't read path environment variable")]
CantReadPathVariable, CantReadPathVariable,
#[snafu(display("Can't add path to environment variable: {}", source))] #[error("Can't add path to environment variable: {}", source)]
CantAddPathToEnvironment { CantAddPathToEnvironment { source: std::env::JoinPathsError },
source: std::env::JoinPathsError, #[error("Can't find version in dotfiles. Please provide a version manually to the command.")]
},
#[snafu(display(
"Can't find version in dotfiles. Please provide a version manually to the command."
))]
CantInferVersion, CantInferVersion,
#[snafu(display("Requested version {} is not currently installed", version))] #[error("Requested version {} is not currently installed", version)]
VersionNotFound { VersionNotFound { version: UserVersion },
version: UserVersion, #[error(transparent)]
},
ApplicableVersionError { ApplicableVersionError {
#[from]
source: UserInputError, source: UserInputError,
}, },
#[snafu(display( #[error("Can't read exit code from process.\nMaybe the process was killed using a signal?")]
"Can't read exit code from process.\nMaybe the process was killed using a signal?"
))]
CantReadProcessExitCode, CantReadProcessExitCode,
#[snafu(display("{}", source))] #[error("{}", source)]
CantGetCurrentVersion { CantGetCurrentVersion {
#[source]
source: current_version::Error, source: current_version::Error,
}, },
#[snafu(display("No current version. Please run `fnm use <version>` and retry."))] #[error("No current version. Please run `fnm use <version>` and retry.")]
NoCurrentVersion, NoCurrentVersion,
#[snafu(display("command not provided. Please provide a command to run as an argument, like {} or {}.\n{} {}", "node".italic(), "bash".italic(), "example:".yellow().bold(), "fnm exec --using=12 node --version".italic().yellow()))] #[error("command not provided. Please provide a command to run as an argument, like {} or {}.\n{} {}", "node".italic(), "bash".italic(), "example:".yellow().bold(), "fnm exec --using=12 node --version".italic().yellow())]
NoBinaryProvided, NoBinaryProvided,
} }

70
src/commands/install.rs

@ -10,16 +10,15 @@ use crate::version::Version;
use crate::version_files::get_user_version_for_directory; use crate::version_files::get_user_version_for_directory;
use colored::Colorize; use colored::Colorize;
use log::debug; use log::debug;
use snafu::{ensure, OptionExt, ResultExt, Snafu}; use thiserror::Error;
use structopt::StructOpt;
#[derive(StructOpt, Debug, Default)] #[derive(clap::Parser, Debug, Default)]
pub struct Install { pub struct Install {
/// A version string. Can be a partial semver or a LTS version name by the format lts/NAME /// A version string. Can be a partial semver or a LTS version name by the format lts/NAME
pub version: Option<UserVersion>, pub version: Option<UserVersion>,
/// Install latest LTS /// Install latest LTS
#[structopt(long, conflicts_with = "version")] #[clap(long, conflicts_with = "version")]
pub lts: bool, pub lts: bool,
} }
@ -50,21 +49,20 @@ impl super::command::Command for Install {
let current_version = self let current_version = self
.version()? .version()?
.or_else(|| get_user_version_for_directory(current_dir)) .or_else(|| get_user_version_for_directory(current_dir, config))
.context(CantInferVersion)?; .ok_or(Error::CantInferVersion)?;
let version = match current_version.clone() { let version = match current_version.clone() {
UserVersion::Full(Version::Semver(actual_version)) => Version::Semver(actual_version), UserVersion::Full(Version::Semver(actual_version)) => Version::Semver(actual_version),
UserVersion::Full(v @ (Version::Bypassed | Version::Alias(_))) => { UserVersion::Full(v @ (Version::Bypassed | Version::Alias(_))) => {
ensure!(false, UninstallableVersion { version: v }); return Err(Error::UninstallableVersion { version: v });
unreachable!();
} }
UserVersion::Full(Version::Lts(lts_type)) => { UserVersion::Full(Version::Lts(lts_type)) => {
let available_versions: Vec<_> = remote_node_index::list(&config.node_dist_mirror) let available_versions: Vec<_> = remote_node_index::list(&config.node_dist_mirror)
.context(CantListRemoteVersions)?; .map_err(|source| Error::CantListRemoteVersions { source })?;
let picked_version = lts_type let picked_version = lts_type
.pick_latest(&available_versions) .pick_latest(&available_versions)
.with_context(|| CantFindRelevantLts { .ok_or_else(|| Error::CantFindRelevantLts {
lts_type: lts_type.clone(), lts_type: lts_type.clone(),
})? })?
.version .version
@ -78,14 +76,14 @@ impl super::command::Command for Install {
} }
current_version => { current_version => {
let available_versions: Vec<_> = remote_node_index::list(&config.node_dist_mirror) let available_versions: Vec<_> = remote_node_index::list(&config.node_dist_mirror)
.context(CantListRemoteVersions)? .map_err(|source| Error::CantListRemoteVersions { source })?
.drain(..) .drain(..)
.map(|x| x.version) .map(|x| x.version)
.collect(); .collect();
current_version current_version
.to_version(&available_versions, config) .to_version(&available_versions, config)
.context(CantFindNodeVersion { .ok_or(Error::CantFindNodeVersion {
requested_version: current_version, requested_version: current_version,
})? })?
.clone() .clone()
@ -113,7 +111,7 @@ impl super::command::Command for Install {
Err(err @ DownloaderError::VersionAlreadyInstalled { .. }) => { Err(err @ DownloaderError::VersionAlreadyInstalled { .. }) => {
outln!(config, Error, "{} {}", "warning:".bold().yellow(), err); outln!(config, Error, "{} {}", "warning:".bold().yellow(), err);
} }
other_err => other_err.context(DownloadError)?, other_err => other_err.map_err(|source| Error::DownloadError { source })?,
}; };
if let UserVersion::Full(Version::Lts(lts_type)) = current_version { if let UserVersion::Full(Version::Lts(lts_type)) = current_version {
@ -123,51 +121,41 @@ impl super::command::Command for Install {
alias_name.cyan(), alias_name.cyan(),
version.v_str().cyan() version.v_str().cyan()
); );
create_alias(config, &alias_name, &version).context(IoError)?; create_alias(config, &alias_name, &version)?;
} }
if !config.default_version_dir().exists() { if !config.default_version_dir().exists() {
debug!("Tagging {} as the default version", version.v_str().cyan()); debug!("Tagging {} as the default version", version.v_str().cyan());
create_alias(config, "default", &version).context(IoError)?; create_alias(config, "default", &version)?;
} }
Ok(()) Ok(())
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[snafu(display("Can't download the requested binary: {}", source))] #[error("Can't download the requested binary: {}", source)]
DownloadError { DownloadError { source: DownloaderError },
source: DownloaderError, #[error(transparent)]
},
IoError { IoError {
#[from]
source: std::io::Error, source: std::io::Error,
}, },
#[snafu(display( #[error("Can't find version in dotfiles. Please provide a version manually to the command.")]
"Can't find version in dotfiles. Please provide a version manually to the command."
))]
CantInferVersion, CantInferVersion,
#[snafu(display("Having a hard time listing the remote versions: {}", source))] #[error("Having a hard time listing the remote versions: {}", source)]
CantListRemoteVersions { CantListRemoteVersions { source: crate::http::Error },
source: crate::http::Error, #[error(
},
#[snafu(display(
"Can't find a Node version that matches {} in remote", "Can't find a Node version that matches {} in remote",
requested_version requested_version
))] )]
CantFindNodeVersion { CantFindNodeVersion { requested_version: UserVersion },
requested_version: UserVersion, #[error("Can't find relevant LTS named {}", lts_type)]
}, CantFindRelevantLts { lts_type: crate::lts::LtsType },
#[snafu(display("Can't find relevant LTS named {}", lts_type))] #[error("The requested version is not installable: {}", version.v_str())]
CantFindRelevantLts { UninstallableVersion { version: Version },
lts_type: crate::lts::LtsType, #[error("Too many versions provided. Please don't use --lts with a version string.")]
},
#[snafu(display("The requested version is not installable: {}", version.v_str()))]
UninstallableVersion {
version: Version,
},
#[snafu(display("Too many versions provided. Please don't use --lts with a version string."))]
TooManyVersionsProvided, TooManyVersionsProvided,
} }

20
src/commands/ls_local.rs

@ -3,11 +3,10 @@ use crate::config::FnmConfig;
use crate::current_version::current_version; use crate::current_version::current_version;
use crate::version::Version; use crate::version::Version;
use colored::Colorize; use colored::Colorize;
use snafu::{ResultExt, Snafu};
use std::collections::HashMap; use std::collections::HashMap;
use structopt::StructOpt; use thiserror::Error;
#[derive(StructOpt, Debug)] #[derive(clap::Parser, Debug)]
pub struct LsLocal {} pub struct LsLocal {}
impl super::command::Command for LsLocal { impl super::command::Command for LsLocal {
@ -15,16 +14,17 @@ impl super::command::Command for LsLocal {
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> { fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let base_dir = config.installations_dir(); let base_dir = config.installations_dir();
let mut versions = let mut versions = crate::installed_versions::list(base_dir)
crate::installed_versions::list(base_dir).context(CantListLocallyInstalledVersion)?; .map_err(|source| Error::CantListLocallyInstalledVersion { source })?;
versions.insert(0, Version::Bypassed); versions.insert(0, Version::Bypassed);
versions.sort(); versions.sort();
let aliases_hash = generate_aliases_hash(config).context(CantReadAliases)?; let aliases_hash =
generate_aliases_hash(config).map_err(|source| Error::CantReadAliases { source })?;
let curr_version = current_version(config).ok().flatten(); let curr_version = current_version(config).ok().flatten();
for version in versions { for version in versions {
let version_aliases = match aliases_hash.get(&version.v_str()) { let version_aliases = match aliases_hash.get(&version.v_str()) {
None => "".into(), None => String::new(),
Some(versions) => { Some(versions) => {
let version_string = versions let version_string = versions
.iter() .iter()
@ -60,12 +60,12 @@ fn generate_aliases_hash(config: &FnmConfig) -> std::io::Result<HashMap<String,
Ok(hashmap) Ok(hashmap)
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[snafu(display("Can't list locally installed versions: {}", source))] #[error("Can't list locally installed versions: {}", source)]
CantListLocallyInstalledVersion { CantListLocallyInstalledVersion {
source: crate::installed_versions::Error, source: crate::installed_versions::Error,
}, },
#[snafu(display("Can't read aliases: {}", source))] #[error("Can't read aliases: {}", source)]
CantReadAliases { source: std::io::Error }, CantReadAliases { source: std::io::Error },
} }

15
src/commands/ls_remote.rs

@ -1,16 +1,15 @@
use crate::config::FnmConfig; use crate::config::FnmConfig;
use crate::remote_node_index; use crate::remote_node_index;
use snafu::{ResultExt, Snafu}; use thiserror::Error;
use structopt::StructOpt;
#[derive(StructOpt, Debug)] #[derive(clap::Parser, Debug)]
pub struct LsRemote {} pub struct LsRemote {}
impl super::command::Command for LsRemote { impl super::command::Command for LsRemote {
type Error = Error; type Error = Error;
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> { fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let all_versions = remote_node_index::list(&config.node_dist_mirror).context(HttpError)?; let all_versions = remote_node_index::list(&config.node_dist_mirror)?;
for version in all_versions { for version in all_versions {
print!("{}", version.version); print!("{}", version.version);
@ -24,7 +23,11 @@ impl super::command::Command for LsRemote {
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
HttpError { source: crate::http::Error }, #[error(transparent)]
HttpError {
#[from]
source: crate::http::Error,
},
} }

16
src/commands/unalias.rs

@ -3,10 +3,9 @@ use crate::fs::remove_symlink_dir;
use crate::user_version::UserVersion; use crate::user_version::UserVersion;
use crate::version::Version; use crate::version::Version;
use crate::{choose_version_for_user_input, config::FnmConfig}; use crate::{choose_version_for_user_input, config::FnmConfig};
use snafu::{OptionExt, ResultExt, Snafu}; use thiserror::Error;
use structopt::StructOpt;
#[derive(StructOpt, Debug)] #[derive(clap::Parser, Debug)]
pub struct Unalias { pub struct Unalias {
pub(crate) requested_alias: String, pub(crate) requested_alias: String,
} }
@ -21,20 +20,21 @@ impl Command for Unalias {
) )
.ok() .ok()
.flatten() .flatten()
.with_context(|| AliasNotFound { .ok_or(Error::AliasNotFound {
requested_alias: self.requested_alias, requested_alias: self.requested_alias,
})?; })?;
remove_symlink_dir(&requested_version.path()).context(CantDeleteSymlink)?; remove_symlink_dir(requested_version.path())
.map_err(|source| Error::CantDeleteSymlink { source })?;
Ok(()) Ok(())
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[snafu(display("Can't delete symlink: {}", source))] #[error("Can't delete symlink: {}", source)]
CantDeleteSymlink { source: std::io::Error }, CantDeleteSymlink { source: std::io::Error },
#[snafu(display("Requested alias {} not found", requested_alias))] #[error("Requested alias {} not found", requested_alias)]
AliasNotFound { requested_alias: String }, AliasNotFound { requested_alias: String },
} }

72
src/commands/uninstall.rs

@ -8,10 +8,9 @@ use crate::version::Version;
use crate::version_files::get_user_version_for_directory; use crate::version_files::get_user_version_for_directory;
use colored::Colorize; use colored::Colorize;
use log::debug; use log::debug;
use snafu::{ensure, OptionExt, ResultExt, Snafu}; use thiserror::Error;
use structopt::StructOpt;
#[derive(StructOpt, Debug)] #[derive(clap::Parser, Debug)]
pub struct Uninstall { pub struct Uninstall {
version: Option<UserVersion>, version: Option<UserVersion>,
} }
@ -20,49 +19,48 @@ impl Command for Uninstall {
type Error = Error; type Error = Error;
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> { fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let all_versions = let all_versions = installed_versions::list(config.installations_dir())
installed_versions::list(config.installations_dir()).context(VersionListingError)?; .map_err(|source| Error::VersionListingError { source })?;
let requested_version = self let requested_version = self
.version .version
.or_else(|| { .or_else(|| {
let current_dir = std::env::current_dir().unwrap(); let current_dir = std::env::current_dir().unwrap();
get_user_version_for_directory(current_dir) get_user_version_for_directory(current_dir, config)
}) })
.context(CantInferVersion)?; .ok_or(Error::CantInferVersion)?;
ensure!( if matches!(requested_version, UserVersion::Full(Version::Bypassed)) {
!matches!(requested_version, UserVersion::Full(Version::Bypassed)), return Err(Error::CantUninstallSystemVersion);
CantUninstallSystemVersion }
);
let available_versions: Vec<&Version> = all_versions let available_versions: Vec<&Version> = all_versions
.iter() .iter()
.filter(|v| requested_version.matches(v, config)) .filter(|v| requested_version.matches(v, config))
.collect(); .collect();
ensure!( if available_versions.len() >= 2 {
available_versions.len() < 2, return Err(Error::PleaseBeMoreSpecificToDelete {
PleaseBeMoreSpecificToDelete {
matched_versions: available_versions matched_versions: available_versions
.iter() .iter()
.map(|x| x.v_str()) .map(std::string::ToString::to_string)
.collect::<Vec<_>>() .collect(),
} });
); }
let version = requested_version let version = requested_version
.to_version(&all_versions, config) .to_version(&all_versions, config)
.context(CantFindVersion)?; .ok_or(Error::CantFindVersion)?;
let matching_aliases = version.find_aliases(config).context(IoError)?; let matching_aliases = version.find_aliases(config)?;
let root_path = version let root_path = version
.root_path(config) .root_path(config)
.with_context(|| RootPathNotFound { .ok_or_else(|| Error::RootPathNotFound {
version: version.clone(), version: version.clone(),
})?; })?;
debug!("Removing Node version from {:?}", root_path); debug!("Removing Node version from {:?}", root_path);
std::fs::remove_dir_all(root_path).context(CantDeleteNodeVersion)?; std::fs::remove_dir_all(root_path)
.map_err(|source| Error::CantDeleteNodeVersion { source })?;
outln!( outln!(
config, config,
Info, Info,
@ -72,7 +70,8 @@ impl Command for Uninstall {
for alias in matching_aliases { for alias in matching_aliases {
debug!("Removing alias from {:?}", alias.path()); debug!("Removing alias from {:?}", alias.path());
remove_symlink_dir(alias.path()).context(CantDeleteSymlink)?; remove_symlink_dir(alias.path())
.map_err(|source| Error::CantDeleteSymlink { source })?;
outln!( outln!(
config, config,
Info, Info,
@ -85,26 +84,27 @@ impl Command for Uninstall {
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[snafu(display("Can't get locally installed versions: {}", source))] #[error("Can't get locally installed versions: {}", source)]
VersionListingError { source: installed_versions::Error }, VersionListingError { source: installed_versions::Error },
#[snafu(display( #[error("Can't find version in dotfiles. Please provide a version manually to the command.")]
"Can't find version in dotfiles. Please provide a version manually to the command."
))]
CantInferVersion, CantInferVersion,
#[snafu(display("Can't uninstall system version"))] #[error("Can't uninstall system version")]
CantUninstallSystemVersion, CantUninstallSystemVersion,
#[snafu(display("Too many versions had matched, please be more specific.\nFound {} matching versions, expected 1:\n{}", matched_versions.len(), matched_versions.iter().map(|v| format!("* {}", v)).collect::<Vec<_>>().join("\n")))] #[error("Too many versions had matched, please be more specific.\nFound {} matching versions, expected 1:\n{}", matched_versions.len(), matched_versions.iter().map(|v| format!("* {}", v)).collect::<Vec<_>>().join("\n"))]
PleaseBeMoreSpecificToDelete { matched_versions: Vec<String> }, PleaseBeMoreSpecificToDelete { matched_versions: Vec<String> },
#[snafu(display("Can't find a matching version"))] #[error("Can't find a matching version")]
CantFindVersion, CantFindVersion,
#[snafu(display("Root path not found for version {}", version))] #[error("Root path not found for version {}", version)]
RootPathNotFound { version: Version }, RootPathNotFound { version: Version },
#[snafu(display("io error: {}", source))] #[error("io error: {}", source)]
IoError { source: std::io::Error }, IoError {
#[snafu(display("Can't delete Node.js version: {}", source))] #[from]
source: std::io::Error,
},
#[error("Can't delete Node.js version: {}", source)]
CantDeleteNodeVersion { source: std::io::Error }, CantDeleteNodeVersion { source: std::io::Error },
#[snafu(display("Can't delete symlink: {}", source))] #[error("Can't delete symlink: {}", source)]
CantDeleteSymlink { source: std::io::Error }, CantDeleteSymlink { source: std::io::Error },
} }

140
src/commands/use.rs

@ -1,65 +1,75 @@
use super::command::Command; use super::command::Command;
use super::install::Install; use super::install::Install;
use crate::current_version::current_version;
use crate::fs; use crate::fs;
use crate::installed_versions; use crate::installed_versions;
use crate::outln; use crate::outln;
use crate::system_version; use crate::system_version;
use crate::user_version::UserVersion; use crate::user_version::UserVersion;
use crate::version::Version; use crate::version::Version;
use crate::version_file_strategy::VersionFileStrategy;
use crate::version_switch_strategy::VersionSwitchStrategy;
use crate::{config::FnmConfig, user_version_reader::UserVersionReader}; use crate::{config::FnmConfig, user_version_reader::UserVersionReader};
use colored::Colorize; use colored::Colorize;
use snafu::{ensure, OptionExt, ResultExt, Snafu}; use std::path::Path;
use structopt::StructOpt; use thiserror::Error;
#[derive(StructOpt, Debug)] #[derive(clap::Parser, Debug)]
pub struct Use { pub struct Use {
version: Option<UserVersionReader>, version: Option<UserVersionReader>,
/// Install the version if it isn't installed yet /// Install the version if it isn't installed yet
#[structopt(long)] #[clap(long)]
install_if_missing: bool, install_if_missing: bool,
/// Don't output a message identifying the version being used
/// if it will not change due to execution of this command
#[clap(long)]
silent_if_unchanged: bool,
} }
impl Command for Use { impl Command for Use {
type Error = Error; type Error = Error;
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> { fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let multishell_path = config.multishell_path().context(FnmEnvWasNotSourced)?; let multishell_path = config.multishell_path().ok_or(Error::FnmEnvWasNotSourced)?;
warn_if_multishell_path_not_in_path_env_var(multishell_path, config); warn_if_multishell_path_not_in_path_env_var(multishell_path, config);
let all_versions = let all_versions = installed_versions::list(config.installations_dir())
installed_versions::list(config.installations_dir()).context(VersionListingError)?; .map_err(|source| Error::VersionListingError { source })?;
let requested_version = self let requested_version = self
.version .version
.unwrap_or_else(|| { .unwrap_or_else(|| {
let current_dir = std::env::current_dir().unwrap(); let current_dir = std::env::current_dir().unwrap();
UserVersionReader::Path(current_dir) UserVersionReader::Path(current_dir)
}) })
.into_user_version() .into_user_version(config)
.context(CantInferVersion)?; .ok_or_else(|| match config.version_file_strategy() {
VersionFileStrategy::Local => InferVersionError::Local,
VersionFileStrategy::Recursive => InferVersionError::Recursive,
})
.map_err(|source| Error::CantInferVersion { source })?;
let version_path = if let UserVersion::Full(Version::Bypassed) = requested_version { let (message, version_path) = if let UserVersion::Full(Version::Bypassed) =
outln!( requested_version
config, {
Info, let message = format!(
"Bypassing fnm: using {} node", "Bypassing fnm: using {} node",
system_version::display_name().cyan() system_version::display_name().cyan()
); );
system_version::path() (message, system_version::path())
} else if let Some(alias_name) = requested_version.alias_name() { } else if let Some(alias_name) = requested_version.alias_name() {
let alias_path = config.aliases_dir().join(&alias_name); let alias_path = config.aliases_dir().join(&alias_name);
let system_path = system_version::path(); let system_path = system_version::path();
if matches!(fs::shallow_read_symlink(&alias_path), Ok(shallow_path) if shallow_path == system_path) if matches!(fs::shallow_read_symlink(&alias_path), Ok(shallow_path) if shallow_path == system_path)
{ {
outln!( let message = format!(
config,
Info,
"Bypassing fnm: using {} node", "Bypassing fnm: using {} node",
system_version::display_name().cyan() system_version::display_name().cyan()
); );
system_path (message, system_path)
} else if alias_path.exists() { } else if alias_path.exists() {
outln!(config, Info, "Using Node for alias {}", alias_name.cyan()); let message = format!("Using Node for alias {}", alias_name.cyan());
alias_path (message, alias_path)
} else { } else {
install_new_version(requested_version, config, self.install_if_missing)?; install_new_version(requested_version, config, self.install_if_missing)?;
return Ok(()); return Ok(());
@ -67,45 +77,67 @@ impl Command for Use {
} else { } else {
let current_version = requested_version.to_version(&all_versions, config); let current_version = requested_version.to_version(&all_versions, config);
if let Some(version) = current_version { if let Some(version) = current_version {
outln!(config, Info, "Using Node {}", version.to_string().cyan()); let version_path = config
config
.installations_dir() .installations_dir()
.join(version.to_string()) .join(version.to_string())
.join("installation") .join("installation");
let message = format!("Using Node {}", version.to_string().cyan());
(message, version_path)
} else { } else {
install_new_version(requested_version, config, self.install_if_missing)?; install_new_version(requested_version, config, self.install_if_missing)?;
return Ok(()); return Ok(());
} }
}; };
replace_symlink(&version_path, multishell_path).context(SymlinkingCreationIssue)?; if !self.silent_if_unchanged || will_version_change(&version_path, config) {
outln!(config, Info, "{}", message);
}
if let Some(multishells_path) = multishell_path.parent() {
std::fs::create_dir_all(multishells_path).map_err(|_err| {
Error::MultishellDirectoryCreationIssue {
path: multishells_path.to_path_buf(),
}
})?;
}
replace_symlink(&version_path, multishell_path)
.map_err(|source| Error::SymlinkingCreationIssue { source })?;
Ok(()) Ok(())
} }
} }
fn will_version_change(resolved_path: &Path, config: &FnmConfig) -> bool {
let current_version_path = current_version(config)
.unwrap_or(None)
.map(|v| v.installation_path(config));
current_version_path.as_deref() != Some(resolved_path)
}
fn install_new_version( fn install_new_version(
requested_version: UserVersion, requested_version: UserVersion,
config: &FnmConfig, config: &FnmConfig,
install_if_missing: bool, install_if_missing: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
ensure!( if !install_if_missing && !should_install_interactively(&requested_version) {
install_if_missing || should_install_interactively(&requested_version), return Err(Error::CantFindVersion {
CantFindVersion { version: requested_version,
version: requested_version });
} }
);
Install { Install {
version: Some(requested_version.clone()), version: Some(requested_version.clone()),
..Install::default() ..Install::default()
} }
.apply(config) .apply(config)
.context(InstallError)?; .map_err(|source| Error::InstallError { source })?;
Use { Use {
version: Some(UserVersionReader::Direct(requested_version)), version: Some(UserVersionReader::Direct(requested_version)),
install_if_missing: true, install_if_missing: true,
silent_if_unchanged: false,
} }
.apply(config)?; .apply(config)?;
@ -119,8 +151,8 @@ fn install_new_version(
/// ///
/// This way, we can create a symlink if it is missing. /// This way, we can create a symlink if it is missing.
fn replace_symlink(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> { fn replace_symlink(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> {
let symlink_deletion_result = fs::remove_symlink_dir(&to); let symlink_deletion_result = fs::remove_symlink_dir(to);
match fs::symlink_dir(&from, &to) { match fs::symlink_dir(from, to) {
ok @ Ok(_) => ok, ok @ Ok(_) => ok,
err @ Err(_) => symlink_deletion_result.and(err), err @ Err(_) => symlink_deletion_result.and(err),
} }
@ -153,6 +185,13 @@ fn warn_if_multishell_path_not_in_path_env_var(
multishell_path: &std::path::Path, multishell_path: &std::path::Path,
config: &FnmConfig, config: &FnmConfig,
) { ) {
if matches!(
config.version_switch_strategy(),
VersionSwitchStrategy::Shims
) {
return;
}
let bin_path = if cfg!(unix) { let bin_path = if cfg!(unix) {
multishell_path.join("bin") multishell_path.join("bin")
} else { } else {
@ -175,27 +214,36 @@ fn warn_if_multishell_path_not_in_path_env_var(
); );
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[snafu(display("Can't create the symlink: {}", source))] #[error("Can't create the symlink: {}", source)]
SymlinkingCreationIssue { source: std::io::Error }, SymlinkingCreationIssue { source: std::io::Error },
#[snafu(display("Can't read the symlink: {}", source))] #[error(transparent)]
SymlinkReadFailed { source: std::io::Error },
#[snafu(display("{}", source))]
InstallError { source: <Install as Command>::Error }, InstallError { source: <Install as Command>::Error },
#[snafu(display("Can't get locally installed versions: {}", source))] #[error("Can't get locally installed versions: {}", source)]
VersionListingError { source: installed_versions::Error }, VersionListingError { source: installed_versions::Error },
#[snafu(display("Requested version {} is not currently installed", version))] #[error("Requested version {} is not currently installed", version)]
CantFindVersion { version: UserVersion }, CantFindVersion { version: UserVersion },
#[snafu(display( #[error(transparent)]
"Can't find version in dotfiles. Please provide a version manually to the command." CantInferVersion {
))] #[from]
CantInferVersion, source: InferVersionError,
#[snafu(display( },
#[error(
"{}\n{}\n{}", "{}\n{}\n{}",
"We can't find the necessary environment variables to replace the Node version.", "We can't find the necessary environment variables to replace the Node version.",
"You should setup your shell profile to evaluate `fnm env`, see https://github.com/Schniz/fnm#shell-setup on how to do this", "You should setup your shell profile to evaluate `fnm env`, see https://github.com/Schniz/fnm#shell-setup on how to do this",
"Check out our documentation for more information: https://fnm.vercel.app" "Check out our documentation for more information: https://fnm.vercel.app"
))] )]
FnmEnvWasNotSourced, FnmEnvWasNotSourced,
#[error("Can't create the multishell directory: {}", path.display())]
MultishellDirectoryCreationIssue { path: std::path::PathBuf },
}
#[derive(Debug, Error)]
pub enum InferVersionError {
#[error("Can't find version in dotfiles. Please provide a version manually to the command.")]
Local,
#[error("Could not find any version to use. Maybe you don't have a default version set?\nTry running `fnm default <VERSION>` to set one,\nor create a .node-version file inside your project to declare a Node.js version.")]
Recursive,
} }

83
src/config.rs

@ -1,19 +1,14 @@
use crate::arch::Arch;
use crate::log_level::LogLevel; use crate::log_level::LogLevel;
use crate::outln;
use crate::path_ext::PathExt; use crate::path_ext::PathExt;
use colored::Colorize; use crate::version_file_strategy::VersionFileStrategy;
use crate::{arch::Arch, version_switch_strategy::VersionSwitchStrategy};
use dirs::{data_dir, home_dir}; use dirs::{data_dir, home_dir};
use std::sync::atomic::{AtomicBool, Ordering};
use structopt::StructOpt;
use url::Url; use url::Url;
static HAS_WARNED_DEPRECATED_BASE_DIR: AtomicBool = AtomicBool::new(false); #[derive(clap::Parser, Debug)]
#[derive(StructOpt, Debug)]
pub struct FnmConfig { pub struct FnmConfig {
/// https://nodejs.org/dist/ mirror /// https://nodejs.org/dist/ mirror
#[structopt( #[clap(
long, long,
env = "FNM_NODE_DIST_MIRROR", env = "FNM_NODE_DIST_MIRROR",
default_value = "https://nodejs.org/dist", default_value = "https://nodejs.org/dist",
@ -23,7 +18,7 @@ pub struct FnmConfig {
pub node_dist_mirror: Url, pub node_dist_mirror: Url,
/// The root directory of fnm installations. /// The root directory of fnm installations.
#[structopt( #[clap(
long = "fnm-dir", long = "fnm-dir",
env = "FNM_DIR", env = "FNM_DIR",
global = true, global = true,
@ -34,16 +29,11 @@ pub struct FnmConfig {
/// Where the current node version link is stored. /// Where the current node version link is stored.
/// This value will be populated automatically by evaluating /// This value will be populated automatically by evaluating
/// `fnm env` in your shell profile. Read more about it using `fnm help env` /// `fnm env` in your shell profile. Read more about it using `fnm help env`
#[structopt( #[clap(long, env = "FNM_MULTISHELL_PATH", hide_env_values = true, hide = true)]
long,
env = "FNM_MULTISHELL_PATH",
hide_env_values = true,
hidden = true
)]
multishell_path: Option<std::path::PathBuf>, multishell_path: Option<std::path::PathBuf>,
/// The log level of fnm commands /// The log level of fnm commands
#[structopt( #[clap(
long, long,
env = "FNM_LOGLEVEL", env = "FNM_LOGLEVEL",
default_value = "info", default_value = "info",
@ -55,14 +45,39 @@ pub struct FnmConfig {
/// Override the architecture of the installed Node binary. /// Override the architecture of the installed Node binary.
/// Defaults to arch of fnm binary. /// Defaults to arch of fnm binary.
#[structopt( #[clap(
long, long,
env = "FNM_ARCH", env = "FNM_ARCH",
default_value, default_value_t,
global = true, global = true,
hide_env_values = true hide_env_values = true,
hide_default_value = true
)] )]
pub arch: Arch, pub arch: Arch,
/// A strategy for how to resolve the Node version. Used whenever `fnm use` or `fnm install` is
/// called without a version, or when `--use-on-cd` is configured on evaluation.
///
/// * `local`: Use the local version of Node defined within the current directory
///
/// * `recursive`: Use the version of Node defined within the current directory and all parent directories
#[clap(
long,
env = "FNM_VERSION_FILE_STRATEGY",
possible_values = VersionFileStrategy::possible_values(),
default_value = "local",
global = true,
hide_env_values = true,
)]
version_file_strategy: VersionFileStrategy,
#[clap(
env = "FNM_VERSION_SWITCH_STRATEGY",
hide = true,
possible_values = VersionSwitchStrategy::possible_values(),
default_value = "path-symlink"
)]
version_switch_strategy: VersionSwitchStrategy,
} }
impl Default for FnmConfig { impl Default for FnmConfig {
@ -73,11 +88,17 @@ impl Default for FnmConfig {
multishell_path: None, multishell_path: None,
log_level: LogLevel::Info, log_level: LogLevel::Info,
arch: Arch::default(), arch: Arch::default(),
version_file_strategy: VersionFileStrategy::default(),
version_switch_strategy: VersionSwitchStrategy::default(),
} }
} }
} }
impl FnmConfig { impl FnmConfig {
pub fn version_file_strategy(&self) -> &VersionFileStrategy {
&self.version_file_strategy
}
pub fn multishell_path(&self) -> Option<&std::path::Path> { pub fn multishell_path(&self) -> Option<&std::path::Path> {
match &self.multishell_path { match &self.multishell_path {
None => None, None => None,
@ -102,24 +123,6 @@ impl FnmConfig {
let modern = data_dir().map(|dir| dir.join("fnm")); let modern = data_dir().map(|dir| dir.join("fnm"));
if let Some(dir) = legacy { if let Some(dir) = legacy {
if !HAS_WARNED_DEPRECATED_BASE_DIR.load(Ordering::SeqCst) {
HAS_WARNED_DEPRECATED_BASE_DIR.store(true, Ordering::SeqCst);
let legacy_str = dir.display().to_string();
let modern_str = modern.map_or("$XDG_DATA_HOME/fnm".to_string(), |path| {
path.display().to_string()
});
outln!(
self, Error,
"{}\n It looks like you have the {} directory on your disk.\n fnm is migrating its default storage location for application data to {}.\n You can read more about it here: {}\n",
"warning:".yellow().bold(),
legacy_str.italic(),
modern_str.italic(),
"https://github.com/schniz/fnm/issues/357".italic()
);
}
return dir; return dir;
} }
@ -144,6 +147,10 @@ impl FnmConfig {
.ensure_exists_silently() .ensure_exists_silently()
} }
pub fn version_switch_strategy(&self) -> &VersionSwitchStrategy {
&self.version_switch_strategy
}
#[cfg(test)] #[cfg(test)]
pub fn with_base_dir(mut self, base_dir: Option<std::path::PathBuf>) -> Self { pub fn with_base_dir(mut self, base_dir: Option<std::path::PathBuf>) -> Self {
self.base_dir = base_dir; self.base_dir = base_dir;

18
src/current_version.rs

@ -1,10 +1,11 @@
use thiserror::Error;
use crate::config::FnmConfig; use crate::config::FnmConfig;
use crate::system_version; use crate::system_version;
use crate::version::Version; use crate::version::Version;
use snafu::{OptionExt, ResultExt, Snafu};
pub fn current_version(config: &FnmConfig) -> Result<Option<Version>, Error> { pub fn current_version(config: &FnmConfig) -> Result<Option<Version>, Error> {
let multishell_path = config.multishell_path().context(EnvNotApplied)?; let multishell_path = config.multishell_path().ok_or(Error::EnvNotApplied)?;
if multishell_path.read_link().ok() == Some(system_version::path()) { if multishell_path.read_link().ok() == Some(system_version::path()) {
return Ok(Some(Version::Bypassed)); return Ok(Some(Version::Bypassed));
@ -19,20 +20,21 @@ pub fn current_version(config: &FnmConfig) -> Result<Option<Version>, Error> {
.expect("Can't get filename") .expect("Can't get filename")
.to_str() .to_str()
.expect("Invalid OS string"); .expect("Invalid OS string");
let version = Version::parse(file_name).context(VersionError { version: file_name })?; let version = Version::parse(file_name).map_err(|source| Error::VersionError {
source,
version: file_name.to_string(),
})?;
Ok(Some(version)) Ok(Some(version))
} else { } else {
Ok(None) Ok(None)
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[snafu(display( #[error("`fnm env` was not applied in this context.\nCan't find fnm's environment variables")]
"`fnm env` was not applied in this context.\nCan't find fnm's environment variables"
))]
EnvNotApplied, EnvNotApplied,
#[snafu(display("Can't read the version as a valid semver"))] #[error("Can't read the version as a valid semver")]
VersionError { VersionError {
source: semver::Error, source: semver::Error,
version: String, version: String,

9
src/default_version.rs

@ -0,0 +1,9 @@
use crate::config::FnmConfig;
use crate::version::Version;
use std::str::FromStr;
pub fn find_default_version(config: &FnmConfig) -> Option<Version> {
let version_path = config.default_version_dir().canonicalize().ok()?;
let file_name = version_path.parent()?.file_name()?;
Version::from_str(file_name.to_str()?).ok()?.into()
}

26
src/directories.rs

@ -0,0 +1,26 @@
use std::path::PathBuf;
fn xdg_dir(env: &str) -> Option<PathBuf> {
let env_var = std::env::var(env).ok()?;
Some(PathBuf::from(env_var))
}
fn state_dir() -> Option<PathBuf> {
xdg_dir("XDG_STATE_HOME").or_else(dirs::state_dir)
}
fn cache_dir() -> Option<PathBuf> {
xdg_dir("XDG_CACHE_HOME").or_else(dirs::cache_dir)
}
fn runtime_dir() -> Option<PathBuf> {
xdg_dir("XDG_RUNTIME_DIR").or_else(dirs::runtime_dir)
}
pub fn multishell_storage() -> PathBuf {
runtime_dir()
.or_else(state_dir)
.or_else(cache_dir)
.unwrap_or_else(std::env::temp_dir)
.join("fnm_multishells")
}

67
src/downloader.rs

@ -4,38 +4,34 @@ use crate::archive::{Error as ExtractError, Extract};
use crate::directory_portal::DirectoryPortal; use crate::directory_portal::DirectoryPortal;
use crate::version::Version; use crate::version::Version;
use log::debug; use log::debug;
use snafu::{ensure, OptionExt, ResultExt, Snafu};
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use thiserror::Error;
use url::Url; use url::Url;
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[error(transparent)]
HttpError { HttpError {
#[from]
source: crate::http::Error, source: crate::http::Error,
}, },
#[error(transparent)]
IoError { IoError {
#[from]
source: std::io::Error, source: std::io::Error,
}, },
#[snafu(display("Can't extract the file: {}", source))] #[error("Can't extract the file: {}", source)]
CantExtractFile { CantExtractFile {
#[from]
source: ExtractError, source: ExtractError,
}, },
#[snafu(display("The downloaded archive is empty"))] #[error("The downloaded archive is empty")]
TarIsEmpty, TarIsEmpty,
#[snafu(display( #[error("{} for {} not found upstream.\nYou can `fnm ls-remote` to see available versions or try a different `--arch`.", version, arch)]
"{} for {} not found upstream.\nYou can `fnm ls-remote` to see available versions or try a different `--arch`.", VersionNotFound { version: Version, arch: Arch },
version, #[error("Version already installed at {:?}", path)]
arch VersionAlreadyInstalled { path: PathBuf },
))]
VersionNotFound {
version: Version,
arch: Arch,
},
#[snafu(display("Version already installed at {:?}", path))]
VersionAlreadyInstalled {
path: PathBuf,
},
} }
#[cfg(unix)] #[cfg(unix)]
@ -75,7 +71,7 @@ pub fn extract_archive_into<P: AsRef<Path>>(
let extractor = archive::TarXz::new(response); let extractor = archive::TarXz::new(response);
#[cfg(windows)] #[cfg(windows)]
let extractor = archive::Zip::new(response); let extractor = archive::Zip::new(response);
extractor.extract_into(path).context(CantExtractFile)?; extractor.extract_into(path)?;
Ok(()) Ok(())
} }
@ -88,23 +84,22 @@ pub fn install_node_dist<P: AsRef<Path>>(
) -> Result<(), Error> { ) -> Result<(), Error> {
let installation_dir = PathBuf::from(installations_dir.as_ref()).join(version.v_str()); let installation_dir = PathBuf::from(installations_dir.as_ref()).join(version.v_str());
ensure!( if installation_dir.exists() {
!installation_dir.exists(), return Err(Error::VersionAlreadyInstalled {
VersionAlreadyInstalled { path: installation_dir,
path: installation_dir });
} }
);
std::fs::create_dir_all(installations_dir.as_ref()).context(IoError)?; std::fs::create_dir_all(installations_dir.as_ref())?;
let temp_installations_dir = installations_dir.as_ref().join(".downloads"); let temp_installations_dir = installations_dir.as_ref().join(".downloads");
std::fs::create_dir_all(&temp_installations_dir).context(IoError)?; std::fs::create_dir_all(&temp_installations_dir)?;
let portal = DirectoryPortal::new_in(&temp_installations_dir, installation_dir); let portal = DirectoryPortal::new_in(&temp_installations_dir, installation_dir);
let url = download_url(node_dist_mirror, version, arch); let url = download_url(node_dist_mirror, version, arch);
debug!("Going to call for {}", &url); debug!("Going to call for {}", &url);
let response = crate::http::get(url.as_str()).context(HttpError)?; let response = crate::http::get(url.as_str())?;
if response.status() == 404 { if response.status() == 404 {
return Err(Error::VersionNotFound { return Err(Error::VersionNotFound {
@ -117,17 +112,15 @@ pub fn install_node_dist<P: AsRef<Path>>(
extract_archive_into(&portal, response)?; extract_archive_into(&portal, response)?;
debug!("Extraction completed"); debug!("Extraction completed");
let installed_directory = std::fs::read_dir(&portal) let installed_directory = std::fs::read_dir(&portal)?
.context(IoError)?
.next() .next()
.context(TarIsEmpty)? .ok_or(Error::TarIsEmpty)??;
.context(IoError)?;
let installed_directory = installed_directory.path(); let installed_directory = installed_directory.path();
let renamed_installation_dir = portal.join("installation"); let renamed_installation_dir = portal.join("installation");
std::fs::rename(installed_directory, renamed_installation_dir).context(IoError)?; std::fs::rename(installed_directory, renamed_installation_dir)?;
portal.teleport().context(IoError)?; portal.teleport()?;
Ok(()) Ok(())
} }
@ -159,13 +152,11 @@ mod tests {
#[test_log::test] #[test_log::test]
fn test_installing_npm() { fn test_installing_npm() {
let installations_dir = tempdir().unwrap(); let installations_dir = tempdir().unwrap();
let npm_path = install_in(installations_dir.path()).join(if cfg!(windows) { let bin_dir = install_in(installations_dir.path());
"npm.cmd" let npm_path = bin_dir.join(if cfg!(windows) { "npm.cmd" } else { "npm" });
} else {
"npm"
});
let stdout = duct::cmd(npm_path.to_str().unwrap(), vec!["--version"]) let stdout = duct::cmd(npm_path.to_str().unwrap(), vec!["--version"])
.env("PATH", bin_dir)
.stdout_capture() .stdout_capture()
.run() .run()
.expect("Can't run npm") .expect("Can't run npm")

28
src/installed_versions.rs

@ -1,11 +1,11 @@
use crate::version::Version; use crate::version::Version;
use snafu::{ResultExt, Snafu};
use std::path::Path; use std::path::Path;
use thiserror::Error;
pub fn list<P: AsRef<Path>>(installations_dir: P) -> Result<Vec<Version>, Error> { pub fn list<P: AsRef<Path>>(installations_dir: P) -> Result<Vec<Version>, Error> {
let mut vec = vec![]; let mut vec = vec![];
for result_entry in installations_dir.as_ref().read_dir().context(IoError)? { for result_entry in installations_dir.as_ref().read_dir()? {
let entry = result_entry.context(IoError)?; let entry = result_entry?;
if entry if entry
.file_name() .file_name()
.to_str() .to_str()
@ -17,19 +17,25 @@ pub fn list<P: AsRef<Path>>(installations_dir: P) -> Result<Vec<Version>, Error>
let path = entry.path(); let path = entry.path();
let filename = path let filename = path
.file_name() .file_name()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound)) .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?
.context(IoError)?
.to_str() .to_str()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound)) .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?;
.context(IoError)?; let version = Version::parse(filename)?;
let version = Version::parse(filename).context(SemverError)?;
vec.push(version); vec.push(version);
} }
Ok(vec) Ok(vec)
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
IoError { source: std::io::Error }, #[error(transparent)]
SemverError { source: semver::Error }, IoError {
#[from]
source: std::io::Error,
},
#[error(transparent)]
SemverError {
#[from]
source: semver::Error,
},
} }

14
src/log_level.rs

@ -22,6 +22,14 @@ impl LogLevel {
} }
} }
pub fn as_str(&self) -> &'static str {
match self {
Self::Quiet => "quiet",
Self::Error => "error",
Self::Info => "info",
}
}
pub fn possible_values() -> &'static [&'static str; 4] { pub fn possible_values() -> &'static [&'static str; 4] {
&["quiet", "info", "all", "error"] &["quiet", "info", "all", "error"]
} }
@ -29,11 +37,7 @@ impl LogLevel {
impl From<LogLevel> for &'static str { impl From<LogLevel> for &'static str {
fn from(level: LogLevel) -> Self { fn from(level: LogLevel) -> Self {
match level { level.as_str()
LogLevel::Quiet => "quiet",
LogLevel::Info => "info",
LogLevel::Error => "error",
}
} }
} }

4
src/main.rs

@ -29,10 +29,14 @@ mod system_version;
mod user_version; mod user_version;
mod user_version_reader; mod user_version_reader;
mod version; mod version;
mod version_file_strategy;
mod version_files; mod version_files;
#[macro_use] #[macro_use]
mod log_level; mod log_level;
mod default_version;
mod directories;
mod version_switch_strategy;
fn main() { fn main() {
env_logger::init(); env_logger::init();

45
src/shell/bash.rs

@ -1,41 +1,54 @@
use crate::version_file_strategy::VersionFileStrategy;
use super::shell::Shell; use super::shell::Shell;
use indoc::indoc; use indoc::{formatdoc, indoc};
use std::path::Path; use std::path::Path;
#[derive(Debug)] #[derive(Debug)]
pub struct Bash; pub struct Bash;
impl Shell for Bash { impl Shell for Bash {
fn to_structopt_shell(&self) -> structopt::clap::Shell { fn to_clap_shell(&self) -> clap_complete::Shell {
structopt::clap::Shell::Bash clap_complete::Shell::Bash
} }
fn path(&self, path: &Path) -> String { fn path(&self, path: &Path) -> anyhow::Result<String> {
format!("export PATH={:?}:$PATH", path.to_str().unwrap()) let path = path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Can't convert path to string"))?;
Ok(format!("export PATH={:?}:$PATH", path))
} }
fn set_env_var(&self, name: &str, value: &str) -> String { fn set_env_var(&self, name: &str, value: &str) -> String {
format!("export {}={:?}", name, value) format!("export {}={:?}", name, value)
} }
fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { fn use_on_cd(&self, config: &crate::config::FnmConfig) -> anyhow::Result<String> {
indoc!( let autoload_hook = match config.version_file_strategy() {
r#" VersionFileStrategy::Local => indoc!(
__fnm_use_if_file_found() { r#"
if [[ -f .node-version || -f .nvmrc ]]; then if [[ -f .node-version || -f .nvmrc ]]; then
fnm use fnm use --silent-if-unchanged
fi fi
} "#
),
VersionFileStrategy::Recursive => r#"fnm use --silent-if-unchanged"#,
};
Ok(formatdoc!(
r#"
__fnm_use_if_file_found() {{
{autoload_hook}
}}
__fnmcd() { __fnmcd() {{
\cd "$@" || return $? \cd "$@" || return $?
__fnm_use_if_file_found __fnm_use_if_file_found
} }}
alias cd=__fnmcd alias cd=__fnmcd
__fnm_use_if_file_found __fnm_use_if_file_found
"# "#,
) autoload_hook = autoload_hook
.into() ))
} }
} }

39
src/shell/fish.rs

@ -1,36 +1,49 @@
use crate::version_file_strategy::VersionFileStrategy;
use super::shell::Shell; use super::shell::Shell;
use indoc::indoc; use indoc::{formatdoc, indoc};
use std::path::Path; use std::path::Path;
#[derive(Debug)] #[derive(Debug)]
pub struct Fish; pub struct Fish;
impl Shell for Fish { impl Shell for Fish {
fn to_structopt_shell(&self) -> structopt::clap::Shell { fn to_clap_shell(&self) -> clap_complete::Shell {
structopt::clap::Shell::Fish clap_complete::Shell::Fish
} }
fn path(&self, path: &Path) -> String { fn path(&self, path: &Path) -> anyhow::Result<String> {
format!("set -gx PATH {:?} $PATH;", path.to_str().unwrap()) let path = path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Can't convert path to string"))?;
Ok(format!("set -gx PATH {:?} $PATH;", path))
} }
fn set_env_var(&self, name: &str, value: &str) -> String { fn set_env_var(&self, name: &str, value: &str) -> String {
format!("set -gx {name} {value:?};", name = name, value = value) format!("set -gx {name} {value:?};", name = name, value = value)
} }
fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { fn use_on_cd(&self, config: &crate::config::FnmConfig) -> anyhow::Result<String> {
indoc!( let autoload_hook = match config.version_file_strategy() {
VersionFileStrategy::Local => indoc!(
r#"
if test -f .node-version -o -f .nvmrc
fnm use --silent-if-unchanged
end
"#
),
VersionFileStrategy::Recursive => r#"fnm use --silent-if-unchanged"#,
};
Ok(formatdoc!(
r#" r#"
function _fnm_autoload_hook --on-variable PWD --description 'Change Node version on directory change' function _fnm_autoload_hook --on-variable PWD --description 'Change Node version on directory change'
status --is-command-substitution; and return status --is-command-substitution; and return
if test -f .node-version -o -f .nvmrc {autoload_hook}
fnm use
end
end end
_fnm_autoload_hook _fnm_autoload_hook
"# "#,
) autoload_hook = autoload_hook
.into() ))
} }
} }

45
src/shell/infer/mod.rs

@ -1,34 +1,21 @@
use super::{Bash, Fish, PowerShell, Shell, WindowsCmd, Zsh}; mod unix;
use log::debug;
use std::ffi::OsStr;
use sysinfo::{ProcessExt, System, SystemExt};
pub fn infer_shell() -> Option<Box<dyn Shell>> { mod windows;
let mut system = System::new();
let mut current_pid = sysinfo::get_current_pid().ok();
while let Some(pid) = current_pid { #[cfg(unix)]
system.refresh_process(pid); pub use self::unix::infer_shell;
if let Some(process) = system.process(pid) { #[cfg(not(unix))]
current_pid = process.parent(); pub use self::windows::infer_shell;
let process_name = process
.exe()
.file_stem()
.and_then(OsStr::to_str)
.map(str::to_lowercase);
let sliced = process_name.as_ref().map(|x| &x[..]);
match sliced {
Some("sh" | "bash") => return Some(Box::from(Bash)),
Some("zsh") => return Some(Box::from(Zsh)),
Some("fish") => return Some(Box::from(Fish)),
Some("pwsh" | "powershell") => return Some(Box::from(PowerShell)),
Some("cmd") => return Some(Box::from(WindowsCmd)),
cmd_name => debug!("binary is not a supported shell: {:?}", cmd_name),
};
} else {
current_pid = None;
}
}
pub(self) fn shell_from_string(shell: &str) -> Option<Box<dyn super::Shell>> {
use super::{Bash, Fish, PowerShell, WindowsCmd, Zsh};
match shell {
"sh" | "bash" => return Some(Box::from(Bash)),
"zsh" => return Some(Box::from(Zsh)),
"fish" => return Some(Box::from(Fish)),
"pwsh" | "powershell" => return Some(Box::from(PowerShell)),
"cmd" => return Some(Box::from(WindowsCmd)),
cmd_name => log::debug!("binary is not a supported shell: {:?}", cmd_name),
};
None None
} }

121
src/shell/infer/unix.rs

@ -0,0 +1,121 @@
#![cfg(unix)]
use crate::shell::Shell;
use log::debug;
use std::io::{Error, ErrorKind};
use thiserror::Error;
#[derive(Debug)]
struct ProcessInfo {
parent_pid: Option<u32>,
command: String,
}
const MAX_ITERATIONS: u8 = 10;
pub fn infer_shell() -> Option<Box<dyn Shell>> {
let mut pid = Some(std::process::id());
let mut visited = 0;
while let Some(current_pid) = pid {
if visited > MAX_ITERATIONS {
return None;
}
let process_info = get_process_info(current_pid)
.map_err(|err| {
debug!("{}", err);
err
})
.ok()?;
let binary = process_info
.command
.trim_start_matches('-')
.split('/')
.last()?;
if let Some(shell) = super::shell_from_string(binary) {
return Some(shell);
}
pid = process_info.parent_pid;
visited += 1;
}
None
}
fn get_process_info(pid: u32) -> Result<ProcessInfo, ProcessInfoError> {
use std::io::{BufRead, BufReader};
use std::process::Command;
let buffer = Command::new("ps")
.arg("-o")
.arg("ppid,comm")
.arg(pid.to_string())
.stdout(std::process::Stdio::piped())
.spawn()?
.stdout
.ok_or_else(|| Error::from(ErrorKind::UnexpectedEof))?;
let mut lines = BufReader::new(buffer).lines();
// skip header line
lines
.next()
.ok_or_else(|| Error::from(ErrorKind::UnexpectedEof))??;
let line = lines
.next()
.ok_or_else(|| Error::from(ErrorKind::NotFound))??;
let mut parts = line.split_whitespace();
let ppid = parts.next().ok_or_else(|| ProcessInfoError::Parse {
expectation: "Can't read the ppid from ps, should be the first item in the table",
got: line.to_string(),
})?;
let command = parts.next().ok_or_else(|| ProcessInfoError::Parse {
expectation: "Can't read the command from ps, should be the second item in the table",
got: line.to_string(),
})?;
Ok(ProcessInfo {
parent_pid: ppid.parse().ok(),
command: command.into(),
})
}
#[derive(Debug, Error)]
enum ProcessInfoError {
#[error("Can't read process info: {source}")]
Io {
#[source]
#[from]
source: std::io::Error,
},
#[error("Can't parse process info output. {expectation}. Got: {got}")]
Parse {
got: String,
expectation: &'static str,
},
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::process::{Command, Stdio};
#[test]
fn test_get_process_info() -> anyhow::Result<()> {
let subprocess = Command::new("bash")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let process_info = get_process_info(subprocess.id());
let parent_pid = process_info.ok().and_then(|x| x.parent_pid);
assert_eq!(parent_pid, Some(std::process::id()));
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save