Browse Source

Aliases and multishell support (#30)

* Use variants instead of a boolean

* Add infrastructure for multishell and aliases: Aliases are required because I want to have a default node version on startup

* create alias command

* fmt

* Added aliases (Fixes #29) and opt-in multishell support (Fixes #19)

* Better docs

* update snapshot

* Some/Fail => Some/None

* add lint-staged

* use refmt and all of prettier are grouped

* System.readdir => Fs.readdir (now uses Lwt)

* use lstat for file_exists: check if symlink exists instead of actual linked file. also, initialize the Random seed on Env

* Remove fish set options that were added in trial and error

* Bootstrap script

* add bootstrap documentation
remotes/origin/add-simple-redirecting-site
Gal Schlezinger 6 years ago committed by GitHub
parent
commit
05e27c6ea3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      .ci/bootstrap
  2. 5
      .ci/pre-commit-hook
  3. 16
      README.md
  4. 4061
      esy.lock/index.json
  5. 4
      esy.lock/opam/ppxlib.0.5.0/opam
  6. 12
      esy.lock/opam/uutf.1.0.2/opam
  7. 6
      esy.lock/opam/yojson.1.6.0/opam
  8. 30
      executable/Alias.re
  9. 48
      executable/Env.re
  10. 64
      executable/FnmApp.re
  11. 17
      executable/ListLocal.re
  12. 49
      executable/Use.re
  13. 1
      feature_tests/fish/run.fish
  14. 31
      feature_tests/multishell/run.sh
  15. 4
      library/Compression.re
  16. 6
      library/Directories.re
  17. 27
      library/Fs.re
  18. 4
      library/Http.re
  19. 6
      library/Opt.re
  20. 6
      library/System.re
  21. 132
      library/Versions.re
  22. 59
      package.json
  23. 1
      test/__snapshots__/Smoke_test.4d362c3c.0.snapshot

11
.ci/bootstrap

@ -0,0 +1,11 @@
#!/bin/bash
GIT_ROOT=$(git rev-parse --show-toplevel)
if [ "$GIT_ROOT" == "" ]; then
echo "Git root cannot be empty"
exit 1
fi
rm -f $GIT_ROOT/.git/hooks/pre-commit &> /dev/null
ln -s $GIT_ROOT/.ci/pre-commit-hook $GIT_ROOT/.git/hooks/pre-commit

5
.ci/pre-commit-hook

@ -0,0 +1,5 @@
#!/bin/bash
set -e
npx lint-staged

16
README.md

@ -11,6 +11,7 @@
</div> </div>
## Features ## Features
:sparkles: Single file, easy installation :sparkles: Single file, easy installation
:rocket: Built with speed in mind :rocket: Built with speed in mind
@ -29,9 +30,9 @@ curl https://raw.githubusercontent.com/Schniz/fnm/master/.ci/install.sh | bash
### Manually ### Manually
* Download the [latest release binary](https://github.com/Schniz/fnm/releases) for your system - Download the [latest release binary](https://github.com/Schniz/fnm/releases) for your system
* Make it available globally on `$PATH` - Make it available globally on `$PATH`
* Add the following line to your `.bashrc`/`.zshrc` file: - Add the following line to your `.bashrc`/`.zshrc` file:
```bash ```bash
eval `fnm env` eval `fnm env`
@ -63,11 +64,15 @@ Lists the installed Node versions.
Lists the Node versions available to download remotely. Lists the Node versions available to download remotely.
### `fnm env [--fish]` ### `fnm env [--multi] [--fish]`
Prints the required shell commands in order to configure your shell, Bash compliant by default.
Prints the required shell commands in order to configure your shell, Bash compliant by default. Provide `--fish` to output the Fish-compliant version. - Providing `--multi` will output the multishell support, allowing a different current Node version per shell
- Providing `--fish` will output the Fish-compliant version.
## Future Plans ## Future Plans
- [ ] Feature: make versions complete the latest: `10` would infer the latest minor and patch versions of node 10. `10.1` would infer the latest patch version of node 10.1 - [ ] Feature: make versions complete the latest: `10` would infer the latest minor and patch versions of node 10. `10.1` would infer the latest patch version of node 10.1
- [ ] Feature: `fnm use --install`, `fnm use --quiet` - [ ] Feature: `fnm use --install`, `fnm use --quiet`
- [ ] Feature: `fnm install lts`? - [ ] Feature: `fnm install lts`?
@ -88,6 +93,7 @@ PRs welcome :tada:
npm install -g esy npm install -g esy
git clone https://github.com/Schniz/fnm.git git clone https://github.com/Schniz/fnm.git
esy install esy install
esy bootstrap
esy build esy build
``` ```

4061
esy.lock/index.json

File diff suppressed because it is too large Load Diff

4
esy.lock/opam/ppxlib.0.5.0/opam

@ -15,12 +15,12 @@ run-test: [
] ]
depends: [ depends: [
"ocaml" {>= "4.04.1"} "ocaml" {>= "4.04.1"}
"base" {>= "v0.11.0"} "base" {>= "v0.11.0" & < "v0.12"}
"dune" {build} "dune" {build}
"ocaml-compiler-libs" {>= "v0.11.0"} "ocaml-compiler-libs" {>= "v0.11.0"}
"ocaml-migrate-parsetree" {>= "1.0.9"} "ocaml-migrate-parsetree" {>= "1.0.9"}
"ppx_derivers" {>= "1.0"} "ppx_derivers" {>= "1.0"}
"stdio" {>= "v0.11.0"} "stdio" {>= "v0.11.0" & < "v0.12"}
"ocamlfind" {with-test} "ocamlfind" {with-test}
] ]
synopsis: "Base library and tools for ppx rewriters" synopsis: "Base library and tools for ppx rewriters"

12
esy.lock/opam/uutf.1.0.1/opam → esy.lock/opam/uutf.1.0.2/opam

@ -20,8 +20,9 @@ build: [[
"ocaml" "pkg/pkg.ml" "build" "ocaml" "pkg/pkg.ml" "build"
"--pinned" "%{pinned}%" "--pinned" "%{pinned}%"
"--with-cmdliner" "%{cmdliner:installed}%" ]] "--with-cmdliner" "%{cmdliner:installed}%" ]]
synopsis: "Non-blocking streaming Unicode codec for OCaml" synopsis: """Non-blocking streaming Unicode codec for OCaml"""
description: """ description: """\
Uutf is a non-blocking streaming codec to decode and encode the UTF-8, Uutf is a non-blocking streaming codec to decode and encode the UTF-8,
UTF-16, UTF-16LE and UTF-16BE encoding schemes. It can efficiently UTF-16, UTF-16LE and UTF-16BE encoding schemes. It can efficiently
work character by character without blocking on IO. Decoders perform work character by character without blocking on IO. Decoders perform
@ -31,8 +32,9 @@ Functions are also provided to fold over the characters of UTF encoded
OCaml string values and to directly encode characters in OCaml OCaml string values and to directly encode characters in OCaml
Buffer.t values. Buffer.t values.
Uutf has no dependency and is distributed under the ISC license.""" Uutf has no dependency and is distributed under the ISC license.
"""
url { url {
src: "http://erratique.ch/software/uutf/releases/uutf-1.0.1.tbz" archive: "http://erratique.ch/software/uutf/releases/uutf-1.0.2.tbz"
checksum: "md5=b8535f974027357094c5cdb4bf03a21b" checksum: "a7c542405a39630c689a82bd7ef2292c"
} }

6
esy.lock/opam/yojson.1.5.0/opam → esy.lock/opam/yojson.1.6.0/opam

@ -9,12 +9,14 @@ build: [
["dune" "subst"] {pinned} ["dune" "subst"] {pinned}
["dune" "build" "-p" name "-j" jobs] ["dune" "build" "-p" name "-j" jobs]
] ]
run-test: [["dune" "runtest" "-p" name "-j" jobs]]
depends: [ depends: [
"ocaml" {>= "4.02.3"} "ocaml" {>= "4.02.3"}
"dune" {build} "dune" {build}
"cppo" {build} "cppo" {build}
"easy-format" "easy-format"
"biniou" {>= "1.2.0"} "biniou" {>= "1.2.0"}
"alcotest" {with-test & >= "0.8.5"}
] ]
synopsis: synopsis:
"Yojson is an optimized parsing and printing library for the JSON format" "Yojson is an optimized parsing and printing library for the JSON format"
@ -31,6 +33,6 @@ The program atdgen can be used to derive OCaml-JSON serializers and
deserializers from type definitions.""" deserializers from type definitions."""
url { url {
src: src:
"https://github.com/ocaml-community/yojson/releases/download/1.5.0/yojson-1.5.0.tbz" "https://github.com/ocaml-community/yojson/releases/download/1.6.0/yojson-1.6.0.tbz"
checksum: "md5=d80de1bacdde292af42f7c78b323da7b" checksum: "md5=8ca16557d3068253cc375452af3bde96"
} }

30
executable/Alias.re

@ -0,0 +1,30 @@
open Fnm;
let run = (~name, ~version) => {
let version = Versions.format(version);
let versionPath = Filename.concat(Directories.nodeVersions, version);
let%lwt versionInstalled = Lwt_unix.file_exists(versionPath);
if (!versionInstalled) {
Console.error(
<Pastel color=Pastel.Red>
"Can't find a version installed in "
versionPath
</Pastel>,
);
exit(1);
};
Console.log(
<Pastel>
"Aliasing "
<Pastel color=Pastel.Cyan> name </Pastel>
" to "
<Pastel color=Pastel.Cyan> version </Pastel>
</Pastel>,
);
let%lwt () = Versions.Aliases.set(~alias=name, ~versionPath);
Lwt.return();
};

48
executable/Env.re

@ -1,14 +1,50 @@
open Fnm; open Fnm;
let run = isFishShell => { let symlinkExists = path => {
if (isFishShell) { try%lwt (Lwt_unix.lstat(path) |> Lwt.map(_ => true)) {
Console.log( | _ => Lwt.return(false)
Printf.sprintf("set PATH %s/bin $PATH", Directories.currentVersion), };
};
let rec makeTemporarySymlink = () => {
let suggestedName =
Filename.concat(
Filename.get_temp_dir_name(),
"fnm-shell-"
++ (Random.int32(9999999 |> Int32.of_int) |> Int32.to_string),
); );
let%lwt exists = symlinkExists(suggestedName);
if (exists) {
let%lwt suggestedName = makeTemporarySymlink();
Lwt.return(suggestedName);
} else { } else {
Console.log( let%lwt _ =
Printf.sprintf("export PATH=%s/bin:$PATH", Directories.currentVersion), Lwt_unix.symlink(
Filename.concat(Directories.defaultVersion, "installation"),
suggestedName,
); );
Lwt.return(suggestedName);
};
};
let run = (~shell, ~multishell) => {
open Lwt;
Random.self_init();
let%lwt path =
multishell
? makeTemporarySymlink() : Lwt.return(Directories.globalCurrentVersion);
switch (shell) {
| System.Shell.Bash =>
Printf.sprintf("export PATH=%s/bin:$PATH", path) |> Console.log;
Printf.sprintf("export FNM_MULTISHELL_PATH=%s", path) |> Console.log;
| System.Shell.Fish =>
Printf.sprintf("set PATH %s/bin $PATH;", path) |> Console.log;
Printf.sprintf("set FNM_MULTISHELL_PATH %s;", path) |> Console.log;
}; };
Lwt.return(); Lwt.return();

64
executable/FnmApp.re

@ -1,11 +1,18 @@
let version = Fnm.Fnm__Package.version; let version = Fnm.Fnm__Package.version;
module Commands = { module Commands = {
let use = version => Lwt_main.run(Use.run(version)); let use = (version, quiet) => Lwt_main.run(Use.run(~version, ~quiet));
let alias = (version, name) => Lwt_main.run(Alias.run(~name, ~version));
let listRemote = () => Lwt_main.run(ListRemote.run()); let listRemote = () => Lwt_main.run(ListRemote.run());
let listLocal = () => Lwt_main.run(ListLocal.run()); let listLocal = () => Lwt_main.run(ListLocal.run());
let install = version => Lwt_main.run(Install.run(~version)); let install = version => Lwt_main.run(Install.run(~version));
let env = isFishShell => Lwt_main.run(Env.run(isFishShell)); let env = (isFishShell, isMultishell) =>
Lwt_main.run(
Env.run(
~shell=Fnm.System.Shell.(isFishShell ? Fish : Bash),
~multishell=isMultishell,
),
);
}; };
open Cmdliner; open Cmdliner;
@ -71,6 +78,11 @@ let use = {
let doc = "Switch to another installed node version"; let doc = "Switch to another installed node version";
let man = []; let man = [];
let quiet = {
let doc = "Don't print stuff";
Arg.(value & flag & info(["quiet"], ~doc));
};
let selectedVersion = { let selectedVersion = {
let doc = "Switch to version $(docv).\nLeave empty to look for value from `.nvmrc`"; let doc = "Switch to version $(docv).\nLeave empty to look for value from `.nvmrc`";
Arg.( Arg.(
@ -79,11 +91,45 @@ let use = {
}; };
( (
Term.(const(Commands.use) $ selectedVersion), Term.(const(Commands.use) $ selectedVersion $ quiet),
Term.info("use", ~version, ~doc, ~exits=Term.default_exits, ~man), Term.info("use", ~version, ~doc, ~exits=Term.default_exits, ~man),
); );
}; };
let alias = {
let doc = "Alias a version";
let sdocs = Manpage.s_common_options;
let man = help_secs;
let selectedVersion = {
let doc = "The version to be aliased";
Arg.(
required
& pos(0, some(string), None)
& info([], ~docv="VERSION", ~doc)
);
};
let aliasName = {
let doc = "The alias name";
Arg.(
required & pos(1, some(string), None) & info([], ~docv="NAME", ~doc)
);
};
(
Term.(const(Commands.alias) $ selectedVersion $ aliasName),
Term.info(
"alias",
~version,
~doc,
~exits=Term.default_exits,
~man,
~sdocs,
),
);
};
let env = { let env = {
let doc = "Show env configurations"; let doc = "Show env configurations";
let sdocs = Manpage.s_common_options; let sdocs = Manpage.s_common_options;
@ -94,8 +140,13 @@ let env = {
Arg.(value & flag & info(["fish"], ~doc)); Arg.(value & flag & info(["fish"], ~doc));
}; };
let isMultishell = {
let doc = "Allow different Node versions for each shell";
Arg.(value & flag & info(["multi"], ~doc));
};
( (
Term.(const(Commands.env) $ isFishShell), Term.(const(Commands.env) $ isFishShell $ isMultishell),
Term.info("env", ~version, ~doc, ~exits=Term.default_exits, ~man, ~sdocs), Term.info("env", ~version, ~doc, ~exits=Term.default_exits, ~man, ~sdocs),
); );
}; };
@ -119,5 +170,8 @@ let defaultCmd = {
}; };
let _ = let _ =
Term.eval_choice(defaultCmd, [install, use, listLocal, listRemote, env]) Term.eval_choice(
defaultCmd,
[install, use, alias, listLocal, listRemote, env],
)
|> Term.exit; |> Term.exit;

17
executable/ListLocal.re

@ -6,22 +6,29 @@ let main = () =>
Versions.Local.( Versions.Local.(
{ {
let%lwt versions = let%lwt versions =
Versions.getInstalledVersions() try%lwt (Versions.getInstalledVersions()) {
|> Result.mapError(_ => Cant_read_local_versions) | _ => Lwt.fail(Cant_read_local_versions)
|> Result.toLwtErr; };
let currentVersion = Versions.getCurrentVersion(); let currentVersion = Versions.getCurrentVersion();
Console.log("The following versions are installed:"); Console.log("The following versions are installed:");
versions versions
|> Array.iter(version => { |> List.iter(version => {
let color = let color =
switch (currentVersion) { switch (currentVersion) {
| None => None | None => None
| Some(x) when x.name == version.name => Some(Pastel.Cyan) | Some(x) when x.name == version.name => Some(Pastel.Cyan)
| Some(_) => None | Some(_) => None
}; };
Console.log(<Pastel ?color> "* " {version.name} </Pastel>); let aliases =
List.length(version.aliases) === 0
? ""
: Printf.sprintf(
" (%s)",
version.aliases |> String.concat(", "),
);
Console.log(<Pastel ?color> "* " {version.name} aliases </Pastel>);
}); });
Lwt.return(); Lwt.return();

49
executable/Use.re

@ -4,18 +4,27 @@ let lwtIgnore = lwt => Lwt.catch(() => lwt, _ => Lwt.return());
exception Version_Not_Installed(string); exception Version_Not_Installed(string);
let switchVersion = version => { let log = (~quiet, arg) =>
let versionDir = Filename.concat(Directories.nodeVersions, version); if (!quiet) {
Console.log(arg);
};
let%lwt _ = let switchVersion = (~version, ~quiet) => {
if%lwt (Lwt_unix.file_exists(versionDir) |> Lwt.map(x => !x)) { open Lwt;
Lwt.fail(Version_Not_Installed(version)); let log = log(~quiet);
let%lwt parsedVersion =
Versions.parse(version) >>= Opt.toLwt(Version_Not_Installed(version));
let%lwt versionPath =
switch (parsedVersion) {
| Local(version) => Versions.Local.toDirectory(version) |> Lwt.return
| Alias(alias) => Versions.Aliases.toDirectory(alias) |> Lwt.return
}; };
let destination = Filename.concat(versionDir, "installation"); let destination = Filename.concat(versionPath, "installation");
let source = Directories.currentVersion; let source = Directories.currentVersion;
Console.log( log(
<Pastel> <Pastel>
"Linking " "Linking "
<Pastel color=Pastel.Cyan> source </Pastel> <Pastel color=Pastel.Cyan> source </Pastel>
@ -25,28 +34,37 @@ let switchVersion = version => {
); );
let%lwt _ = Lwt_unix.unlink(Directories.currentVersion) |> lwtIgnore; let%lwt _ = Lwt_unix.unlink(Directories.currentVersion) |> lwtIgnore;
let%lwt _ = Lwt_unix.symlink(destination, Directories.currentVersion); let%lwt _ = Lwt_unix.symlink(destination, Directories.currentVersion)
and defaultAliasExists = Lwt_unix.file_exists(Directories.defaultVersion);
let%lwt _ =
if (!defaultAliasExists) {
Versions.Aliases.set(~alias="default", ~versionPath=destination);
} else {
Lwt.return();
};
Console.log( log(
<Pastel> "Using " <Pastel color=Pastel.Cyan> version </Pastel> </Pastel>, <Pastel> "Using " <Pastel color=Pastel.Cyan> version </Pastel> </Pastel>,
); );
Lwt.return(); Lwt.return();
}; };
let main = (~version as providedVersion) => { let main = (~version as providedVersion, ~quiet) => {
let%lwt version = let%lwt version =
switch (providedVersion) { switch (providedVersion) {
| Some(version) => Lwt.return(version) | Some(version) => Lwt.return(version)
| None => Nvmrc.getVersion() | None => Nvmrc.getVersion()
}; };
switchVersion(Versions.format(version)); switchVersion(~version, ~quiet);
}; };
let run = version => let run = (~version, ~quiet) =>
try%lwt (main(~version)) { try%lwt (main(~version, ~quiet)) {
| Version_Not_Installed(version) => | Version_Not_Installed(version) =>
Console.log( log(
~quiet,
<Pastel color=Pastel.Red> <Pastel color=Pastel.Red>
"The following version is not installed: " "The following version is not installed: "
version version
@ -54,7 +72,8 @@ let run = version =>
) )
|> Lwt.return |> Lwt.return
| Nvmrc.Version_Not_Provided => | Nvmrc.Version_Not_Provided =>
Console.log( log(
~quiet,
<Pastel color=Pastel.Red> <Pastel color=Pastel.Red>
"No .nvmrc was found in the current directory. Please provide a version number." "No .nvmrc was found in the current directory. Please provide a version number."
</Pastel>, </Pastel>,

1
feature_tests/fish/run.fish

@ -1,6 +1,7 @@
#!/usr/bin/env fish #!/usr/bin/env fish
eval (fnm env --fish) eval (fnm env --fish)
fnm install v8.11.3 fnm install v8.11.3
fnm use v8.11.3 fnm use v8.11.3

31
feature_tests/multishell/run.sh

@ -0,0 +1,31 @@
#!/bin/bash
set -e
eval $(fnm env)
fnm install v8.11.3
fnm install v11.9.0
fnm use v8.11.3
bash -c '
set -e
eval $(fnm env --multi)
fnm use v11.9.0
echo "> verifying version v11.9.0 for child bash"
if [ "$(node -v)" == "v11.9.0" ]; then
echo "Okay!"
else
echo "Node version should be v11.9.0 in the bash fork"
exit 1
fi
'
echo "> verifying version v8.11.3 for parent bash"
if [ "$(node -v)" == "v8.11.3" ]; then
echo "Okay!"
else
echo "Node version should be v8.11.3 in the base bash"
exit 1
fi

4
library/Compression.re

@ -6,8 +6,8 @@ let extractFile = (~into as destination, filepath) => {
~args=[|"-xvf", filepath, "--directory", destination|], ~args=[|"-xvf", filepath, "--directory", destination|],
~stderr=`Dev_null, ~stderr=`Dev_null,
); );
let%lwt files = Fs.readdir(destination) |> Result.toLwt; let%lwt files = Fs.readdir(destination);
let filename = files[0]; let filename = List.hd(files);
Lwt_unix.rename( Lwt_unix.rename(
Filename.concat(destination, filename), Filename.concat(destination, filename),
Filename.concat(destination, "installation"), Filename.concat(destination, "installation"),

6
library/Directories.re

@ -9,5 +9,9 @@ let sfwRoot =
} }
); );
let nodeVersions = Filename.concat(sfwRoot, "node-versions"); let nodeVersions = Filename.concat(sfwRoot, "node-versions");
let currentVersion = Filename.concat(sfwRoot, "current"); let globalCurrentVersion = Filename.concat(sfwRoot, "current");
let currentVersion =
Opt.(Sys.getenv_opt("FNM_MULTISHELL_PATH") or globalCurrentVersion);
let downloads = Filename.concat(sfwRoot, "downloads"); let downloads = Filename.concat(sfwRoot, "downloads");
let aliases = Filename.concat(sfwRoot, "aliases");
let defaultVersion = Filename.concat(aliases, "default");

27
library/Fs.re

@ -1,11 +1,30 @@
open Core; open Core;
let readdir = dir => let readdir = dir => {
switch (Sys.readdir(dir)) { let items = ref([]);
| x => Ok(x) let%lwt dir = Lwt_unix.opendir(dir);
| exception (Sys_error(error)) => Error(error) let iterate = () => {
let%lwt _ =
while%lwt (true) {
let%lwt value = Lwt_unix.readdir(dir);
if (value.[0] != '.') {
items := [value, ...items^];
};
Lwt.return();
};
Lwt.return([]);
}; };
let%lwt items =
try%lwt (iterate()) {
| End_of_file => Lwt.return(items^)
};
let%lwt _ = Lwt_unix.closedir(dir);
Lwt.return(items);
};
let writeFile = (path, contents) => { let writeFile = (path, contents) => {
let%lwt x = Lwt_unix.openfile(path, [Unix.O_RDWR, Unix.O_CREAT], 777); let%lwt x = Lwt_unix.openfile(path, [Unix.O_RDWR, Unix.O_CREAT], 777);
let%lwt _ = let%lwt _ =

4
library/Http.re

@ -14,7 +14,7 @@ let rec getBody = listOfStrings => {
}; };
}; };
let rec getStatus = string => { let getStatus = string => {
List.nth(String.split_on_char(' ', string), 1); List.nth(String.split_on_char(' ', string), 1);
}; };
@ -27,7 +27,7 @@ let verifyStatus = response => {
| 200 => Lwt.return(response) | 200 => Lwt.return(response)
| x when x / 100 == 4 => Lwt.fail(Not_found(response)) | x when x / 100 == 4 => Lwt.fail(Not_found(response))
| x when x / 100 == 5 => Lwt.fail(Internal_server_error(response)) | x when x / 100 == 5 => Lwt.fail(Internal_server_error(response))
| x => Lwt.fail(Unknown_status_code(response)) | _ => Lwt.fail(Unknown_status_code(response))
}; };
}; };

6
library/Opt.re

@ -28,6 +28,12 @@ let toResult = (error, opt) =>
| Some(x) => Ok(x) | Some(x) => Ok(x)
}; };
let toLwt = (error, opt) =>
switch (opt) {
| Some(x) => Lwt.return(x)
| None => Lwt.fail(error)
};
let some = x => Some(x); let some = x => Some(x);
let (or) = (opt, b) => fold(() => b, x => x, opt); let (or) = (opt, b) => fold(() => b, x => x, opt);

6
library/System.re

@ -8,6 +8,12 @@ let unix_exec =
let mkdirp = destination => let mkdirp = destination =>
unix_exec("mkdir", ~stderr=`Dev_null, ~args=[|"-p", destination|]); unix_exec("mkdir", ~stderr=`Dev_null, ~args=[|"-p", destination|]);
module Shell = {
type t =
| Bash
| Fish;
};
module NodeArch = { module NodeArch = {
type t = type t =
| X32 | X32

132
library/Versions.re

@ -1,15 +1,74 @@
module VersionSet = Set.Make(String); module VersionSet = Set.Make(String);
let lwtIgnore = lwt => Lwt.catch(() => lwt, _ => Lwt.return());
module Local = { module Local = {
type t = { type t = {
name: string, name: string,
fullPath: string, fullPath: string,
aliases: list(string),
}; };
let toDirectory = name => Filename.concat(Directories.nodeVersions, name);
}; };
exception Version_not_found(string); exception Version_not_found(string);
exception Already_installed(string); exception Already_installed(string);
module Aliases = {
module VersionAliasMap = Map.Make(String);
type t = {
name: string,
versionName: string,
fullPath: string,
};
let toDirectory = name => Filename.concat(Directories.aliases, name);
let getAll = () => {
let%lwt aliases = Fs.readdir(Directories.aliases);
aliases
|> List.map(alias => {
let fullPath = Filename.concat(Directories.aliases, alias);
{
name: alias,
fullPath,
versionName:
Filename.concat(Directories.aliases, alias)
|> Fs.realpath
|> Filename.basename,
};
})
|> Lwt.return;
};
let byVersion = () => {
let%lwt aliases = getAll();
aliases
|> List.fold_left(
(map, curr) => {
let value =
switch (VersionAliasMap.find_opt(curr.versionName, map)) {
| None => [curr.name]
| Some(arr) => [curr.name, ...arr]
};
VersionAliasMap.add(curr.versionName, value, map);
},
VersionAliasMap.empty,
)
|> Lwt.return;
};
let set = (~alias, ~versionPath) => {
let aliasPath = alias |> toDirectory;
let%lwt _ = System.mkdirp(Directories.aliases);
let%lwt _ = Lwt_unix.unlink(aliasPath) |> lwtIgnore;
let%lwt _ = Lwt_unix.symlink(versionPath, aliasPath);
Lwt.return();
};
};
module Remote = { module Remote = {
type t = { type t = {
name: string, name: string,
@ -30,11 +89,12 @@ module Remote = {
}; };
let getInstalledVersionSet = () => let getInstalledVersionSet = () =>
Fs.readdir(Directories.nodeVersions) Lwt.(
|> Result.fold(_ => [||], x => x) catch(() => Fs.readdir(Directories.nodeVersions), _ => return([]))
|> Array.fold_left( >|= List.fold_left(
(acc, curr) => VersionSet.add(curr, acc), (acc, curr) => VersionSet.add(curr, acc),
VersionSet.empty, VersionSet.empty,
)
); );
let getRelativeLinksFromHTML = html => let getRelativeLinksFromHTML = html =>
@ -105,23 +165,30 @@ let getCurrentVersion = () =>
switch (Fs.realpath(Directories.currentVersion)) { switch (Fs.realpath(Directories.currentVersion)) {
| installationPath => | installationPath =>
let fullPath = Filename.dirname(installationPath); let fullPath = Filename.dirname(installationPath);
Some(Local.{fullPath, name: Core.Filename.basename(fullPath)}); Some(
Local.{fullPath, name: Core.Filename.basename(fullPath), aliases: []},
);
| exception (Unix.Unix_error(_, _, _)) => None | exception (Unix.Unix_error(_, _, _)) => None
}; };
let getInstalledVersions = () => let getInstalledVersions = () =>
Fs.readdir(Directories.nodeVersions) Lwt.(
|> Result.map(x => { {
Array.sort(Remote.compare, x); let%lwt versions =
x; Fs.readdir(Directories.nodeVersions) >|= List.sort(Remote.compare)
}) and aliases = Aliases.byVersion();
|> Result.map(
Array.map(name => versions
|> List.map(name =>
Local.{ Local.{
name, name,
fullPath: Filename.concat(Directories.nodeVersions, name), fullPath: Filename.concat(Directories.nodeVersions, name),
aliases:
Opt.(Aliases.VersionAliasMap.find_opt(name, aliases) or []),
}
)
|> Lwt.return;
} }
),
); );
let getRemoteVersions = () => { let getRemoteVersions = () => {
@ -129,7 +196,7 @@ let getRemoteVersions = () => {
Http.makeRequest("https://nodejs.org/dist/") |> Lwt.map(Http.body); Http.makeRequest("https://nodejs.org/dist/") |> Lwt.map(Http.body);
let versions = bodyString |> Remote.getRelativeLinksFromHTML; let versions = bodyString |> Remote.getRelativeLinksFromHTML;
let installedVersions = Remote.getInstalledVersionSet(); let%lwt installedVersions = Remote.getInstalledVersionSet();
versions versions
|> Core.List.filter(~f=x => |> Core.List.filter(~f=x =>
@ -147,14 +214,35 @@ let getRemoteVersions = () => {
|> Lwt.return; |> Lwt.return;
}; };
type t =
| Alias(string)
| Local(string);
let parse = version => {
let formattedVersion = format(version);
let aliasPath = Aliases.toDirectory(version);
let versionPath = Local.toDirectory(formattedVersion);
let%lwt aliasExists = Lwt_unix.file_exists(aliasPath)
and versionExists = Lwt_unix.file_exists(versionPath);
switch (versionExists, aliasExists) {
| (true, _) => Some(Local(formattedVersion)) |> Lwt.return
| (_, true) => Some(Alias(version)) |> Lwt.return
| (false, false) => Lwt.return_none
};
};
let throwIfInstalled = versionName => { let throwIfInstalled = versionName => {
getInstalledVersions() let%lwt installedVersions =
|> Result.fold( try%lwt (getInstalledVersions()) {
_ => Lwt.return(), | _ => Lwt.return([])
xs => };
Array.exists(x => Local.(x.name == versionName), xs) let isAlreadyInstalled =
|> ( installedVersions |> List.exists(x => Local.(x.name == versionName));
x => x ? Lwt.fail(Already_installed(versionName)) : Lwt.return() if (isAlreadyInstalled) {
), Lwt.fail(Already_installed(versionName));
); } else {
Lwt.return();
};
}; };

59
package.json

@ -13,20 +13,50 @@
}, },
"buildDirs": { "buildDirs": {
"test": { "test": {
"require": ["fnm.lib", "rely.lib"], "require": [
"fnm.lib",
"rely.lib"
],
"main": "TestFnm", "main": "TestFnm",
"name": "TestFnm.exe", "name": "TestFnm.exe",
"ocamloptFlags": ["-linkall", "-g"] "ocamloptFlags": [
"-linkall",
"-g"
]
}, },
"library": { "library": {
"preprocess": ["pps", "lwt_ppx", "ppx_let"], "preprocess": [
"require": ["str", "core", "lwt", "lwt.unix", "lambdasoup", "semver"], "pps",
"lwt_ppx",
"ppx_let"
],
"require": [
"str",
"core",
"lwt",
"lwt.unix",
"lambdasoup",
"semver"
],
"name": "fnm.lib", "name": "fnm.lib",
"namespace": "Fnm" "namespace": "Fnm"
}, },
"executable": { "executable": {
"preprocess": ["pps", "lwt_ppx", "ppx_let"], "preprocess": [
"require": ["core", "cmdliner", "lwt", "lwt.unix", "lambdasoup", "console.lib", "pastel.lib", "fnm.lib"], "pps",
"lwt_ppx",
"ppx_let"
],
"require": [
"core",
"cmdliner",
"lwt",
"lwt.unix",
"lambdasoup",
"console.lib",
"pastel.lib",
"fnm.lib"
],
"main": "FnmApp", "main": "FnmApp",
"name": "fnm.exe" "name": "fnm.exe"
} }
@ -35,6 +65,7 @@
"pesy": "bash -c 'env PESY_MODE=update pesy'", "pesy": "bash -c 'env PESY_MODE=update pesy'",
"update-fnm-package": "node ./.ci/prepare-fnm-package.js", "update-fnm-package": "node ./.ci/prepare-fnm-package.js",
"verify-fnm-package": "node ./.ci/prepare-fnm-package.js --fail-on-difference", "verify-fnm-package": "node ./.ci/prepare-fnm-package.js --fail-on-difference",
"bootstrap": ".ci/bootstrap",
"test": "esy x TestFnm.exe", "test": "esy x TestFnm.exe",
"fmt": "bash -c 'refmt --in-place {library,executable,test}/*.re'" "fmt": "bash -c 'refmt --in-place {library,executable,test}/*.re'"
}, },
@ -59,6 +90,20 @@
"devDependencies": { "devDependencies": {
"@opam/merlin": "*", "@opam/merlin": "*",
"prettier": "*", "prettier": "*",
"jest-diff": "24.0.0" "jest-diff": "24.0.0",
"lint-staged": "*"
},
"lint-staged": {
"*.re": [
"esy refmt --in-place",
"git add"
],
"*.{js,md,json}": [
"esy prettier --write",
"git add"
],
"package.json": [
"esy verify-fnm-package"
]
} }
} }

1
test/__snapshots__/Smoke_test.4d362c3c.0.snapshot

@ -1,3 +1,4 @@
Smoke test › env Smoke test › env
export PATH=<sfwRoot>/current/bin:$PATH export PATH=<sfwRoot>/current/bin:$PATH
export FNM_MULTISHELL_PATH=<sfwRoot>/current

Loading…
Cancel
Save