Skip to content

Commit

Permalink
new: Support being installed globally. (#100)
Browse files Browse the repository at this point in the history
* Add global check.

* Update docs.

* Cleanup.
  • Loading branch information
milesj authored May 20, 2022
1 parent b906fdd commit a63f49b
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 35 deletions.
106 changes: 105 additions & 1 deletion crates/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,110 @@
use moon_cli::run_cli;
use moon_config::constants;
use std::env;
use std::path::{Path, PathBuf};
use tokio::process::Command;

/// Check whether this binary has been installed globally or not.
/// If we encounter an error, simply abort early instead of failing.
async fn is_globally_installed() -> bool {
let exe_path = match env::current_exe() {
Ok(path) => path,
Err(_) => return false,
};

// Global installs happen *outside* of moon's toolchain,
// so we simply assume that they have and are using npm
// in their environment.
let output = match Command::new("npm")
.args(["config", "get", "prefix"])
.output()
.await
{
Ok(out) => out,
Err(_) => return false,
};

// If our executable path starts with the global dir,
// then we must have been installed globally!
let global_dir = PathBuf::from(
String::from_utf8(output.stdout.to_vec())
.unwrap_or_default()
.trim(),
);

exe_path.starts_with(global_dir)
}

fn find_workspace_root(dir: &Path) -> Option<PathBuf> {
let findable = dir.join(constants::CONFIG_DIRNAME);

if findable.exists() {
return Some(dir.to_path_buf());
}

match dir.parent() {
Some(parent_dir) => find_workspace_root(parent_dir),
None => None,
}
}

async fn run_bin(bin_path: &Path, current_dir: &Path) -> Result<(), std::io::Error> {
// Remove the binary path from the current args list
let args = env::args()
.enumerate()
.filter(|(i, arg)| {
if *i == 0 {
!arg.ends_with("moon")
} else {
true
}
})
.map(|(_, arg)| arg)
.collect::<Vec<String>>();

// Execute the found moon binary with the current filtered args
Command::new(bin_path)
.args(args)
.current_dir(current_dir)
.spawn()?
.wait()
.await?;

Ok(())
}

#[tokio::main]
async fn main() {
run_cli().await
let mut run = true;

// Detect if we've been installed globally
if let Ok(current_dir) = env::current_dir() {
if is_globally_installed().await {
// If so, find the workspace root so we can locate the
// locally installed `moon` binary in node modules
if let Some(workspace_root) = find_workspace_root(&current_dir) {
let moon_bin = workspace_root
.join("node_modules")
.join("@moonrepo")
.join("cli")
.join("moon");

// The binary exists! So let's run that one to ensure
// we're running the version pinned in `package.json`,
// instead of this global one!
if moon_bin.exists() {
run = false;

run_bin(&moon_bin, &current_dir)
.await
.expect("Failed to run moon binary");
}
}
}
}

// Otherwise just run the CLI
if run {
run_cli().await
}
}
44 changes: 10 additions & 34 deletions website/docs/install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,47 +184,23 @@ pnpm add --dev -W @moonrepo/cli

### Accessing the `moon` binary

We suggest _not_ installing the package globally, as adding an explicit development dependency with
a version range ensures all developers and machines are using the same version of moon. With that
being said, how should you access `moon`? Choose a pattern below that works best for you or your
team!

1. Update your `PATH` environment variable to include a path to the installed node module. This
would need to be added to every developers shell, and other environments like CI.

```bash title="~/.zshrc"
export PATH="/path/to/repo/node_modules/@moonrepo/cli:$PATH"
```

```shell
moon --help
```

2. Symlink the binary to the root of the repository. This would require every developer to run the
binary from the repository root (which we suggest). This can be automated through a `postinstall`
script.
For developers, we suggest installing the `@moonrepo/cli` package globally, so that you can easily
run `moon` commands from _any_ directory, instead of relying on `package.json` scripts. When using
this approach, the global must be installed with npm (not pnpm or yarn)!

```shell
ln -s ./node_modules/@moonrepo/cli/moon ./moon
npm install -g @moonrep/cli
```

```shell
./moon --help
```

3. Add the binary as a `package.json` script. This pattern is the most familier for frontend
developers, but also requires the script to be ran from the repository root. Jump to the next
section for more information.

```shell
yarn run moon --help
```
Don't worry though, when using the global binary, we ensure the same version of moon is being
executed that was defined as a dependency in the repo.

### Adding a package script

Another way to utilize the `moon` binary would be through a `package.json` script -- but this comes
with a cost -- which is the overhead of launching Node.js and your chosen package manager to execute
the Rust binary, _instead of_ executing the Rust binary directly.
For other scenarios or environments, like CI, `moon` can be ran with through a `package.json` script
-- but this comes with a cost -- which is the overhead of launching Node.js and your chosen package
manager to execute the Rust binary, _instead of_ executing the Rust binary directly. If this is
problematic, feel free to use the global approach below.

```json title="package.json"
{
Expand Down

0 comments on commit a63f49b

Please sign in to comment.