Skip to content

Commit

Permalink
refactor(cli): ♻️ migrated cli to clap (#34)
Browse files Browse the repository at this point in the history
* refactor(cli): ♻️ migrated cli to `clap`

* fix(cli): 🐛 fixed cli cleaning and switched to `fmterr`
  • Loading branch information
arctic-hen7 authored Sep 28, 2021
1 parent 53bb61e commit 83e365c
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 141 deletions.
3 changes: 2 additions & 1 deletion packages/perseus-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ include = [
[dependencies]
include_dir = "0.6"
thiserror = "1"
anyhow = "1"
fmterr = "0.1"
cargo_toml = "0.9"
indicatif = "0.17.0-beta.1" # Not stable, but otherwise error handling is just about impossible
console = "0.14"
serde = "1"
serde_json = "1"
clap = "3.0.0-beta.4"

[lib]
name = "perseus_cli"
Expand Down
169 changes: 73 additions & 96 deletions packages/perseus-cli/src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use clap::Clap;
use fmterr::fmt_err;
use perseus_cli::errors::*;
use perseus_cli::{
build, check_env, delete_artifacts, delete_bad_dir, eject, export, has_ejected, help, prepare,
report_err, serve, PERSEUS_VERSION,
build, check_env, delete_artifacts, delete_bad_dir, eject, export, has_ejected,
parse::{Opts, Subcommand},
prepare, serve,
};
use std::env;
use std::io::Write;
Expand All @@ -26,7 +29,10 @@ fn real_main() -> i32 {
let dir = match dir {
Ok(dir) => dir,
Err(err) => {
report_err!(PrepError::CurrentDirUnavailable { source: err });
eprintln!(
"{}",
fmt_err(&PrepError::CurrentDirUnavailable { source: err })
);
return 1;
}
};
Expand All @@ -37,11 +43,11 @@ fn real_main() -> i32 {
// If something failed, we print the error to `stderr` and return a failure exit code
Err(err) => {
let should_cause_deletion = err_should_cause_deletion(&err);
report_err!(err);
eprintln!("{}", fmt_err(&err));
// Check if the error needs us to delete a partially-formed '.perseus/' directory
if should_cause_deletion {
if let Err(err) = delete_bad_dir(dir) {
report_err!(err);
eprintln!("{}", fmt_err(&err));
}
}
1
Expand All @@ -56,105 +62,76 @@ fn real_main() -> i32 {
fn core(dir: PathBuf) -> Result<i32, Error> {
// Get `stdout` so we can write warnings appropriately
let stdout = &mut std::io::stdout();
// Get the arguments to this program, removing the first one (something like `perseus`)
let mut prog_args: Vec<String> = env::args().collect();
// This will panic if the first argument is not found (which is probably someone trying to fuzz us)
let _executable_name = prog_args.remove(0);
// Check the user's environment to make sure they have prerequisites
check_env()?;

// Warn the user if they're using the CLI single-threaded mode
if env::var("PERSEUS_CLI_SEQUENTIAL").is_ok() {
writeln!(stdout, "Note: the Perseus CLI is running in single-threaded mode, which is less performant on most modern systems. You can switch to multi-threaded mode by unsetting the 'PERSEUS_CLI_SEQUENTIAL' environment variable. If you've deliberately enabled single-threaded mode, you can safely ignore this.\n").expect("Failed to write to stdout.");
}
// Check for special arguments
if matches!(prog_args.get(0), Some(_)) {
if prog_args[0] == "-v" || prog_args[0] == "--version" {
writeln!(stdout, "You are currently running the Perseus CLI v{}! You can see the latest release at https://github.com/arctic-hen7/perseus/releases.", PERSEUS_VERSION).expect("Failed to write to stdout.");
Ok(0)
} else if prog_args[0] == "-h" || prog_args[0] == "--help" {
help(stdout);
Ok(0)
} else {
// Now we can check commands
if prog_args[0] == "build" {
// Set up the '.perseus/' directory if needed
prepare(dir.clone())?;
// Delete old build artifacts

// Parse the CLI options with `clap`
let opts: Opts = Opts::parse();
// Check the user's environment to make sure they have prerequisites
// We do this after any help pages or version numbers have been parsed for snappiness
check_env()?;
// If we're not cleaning up artifacts, create them if needed
if !matches!(opts.subcmd, Subcommand::Clean(_)) {
prepare(dir.clone())?;
}
let exit_code = match opts.subcmd {
Subcommand::Build(build_opts) => {
// Delete old build artifacts
delete_artifacts(dir.clone(), "static")?;
build(dir, build_opts)?
}
Subcommand::Export(export_opts) => {
// Delete old build/exportation artifacts
delete_artifacts(dir.clone(), "static")?;
delete_artifacts(dir.clone(), "exported")?;
export(dir, export_opts)?
}
Subcommand::Serve(serve_opts) => {
// Delete old build artifacts if `--no-build` wasn't specified
if !serve_opts.no_build {
delete_artifacts(dir.clone(), "static")?;
let exit_code = build(dir, &prog_args)?;
Ok(exit_code)
} else if prog_args[0] == "export" {
// Set up the '.perseus/' directory if needed
prepare(dir.clone())?;
// Delete old build/exportation artifacts
}
serve(dir, serve_opts)?
}
Subcommand::Test(test_opts) => {
// This will be used by the subcrates
env::set_var("PERSEUS_TESTING", "true");
// Set up the '.perseus/' directory if needed
prepare(dir.clone())?;
// Delete old build artifacts if `--no-build` wasn't specified
if !test_opts.no_build {
delete_artifacts(dir.clone(), "static")?;
}
serve(dir, test_opts)?
}
Subcommand::Clean(clean_opts) => {
if clean_opts.dist {
// The user only wants to remove distribution artifacts
// We don't delete `render_conf.json` because it's literally impossible for that to be the source of a problem right now
delete_artifacts(dir.clone(), "static")?;
delete_artifacts(dir.clone(), "pkg")?;
delete_artifacts(dir.clone(), "exported")?;
let exit_code = export(dir, &prog_args)?;
Ok(exit_code)
} else if prog_args[0] == "serve" {
// Set up the '.perseus/' directory if needed
prepare(dir.clone())?;
// Delete old build artifacts if `--no-build` wasn't specified
if !prog_args.contains(&"--no-build".to_string()) {
delete_artifacts(dir.clone(), "static")?;
}
let exit_code = serve(dir, &prog_args)?;
Ok(exit_code)
} else if prog_args[0] == "test" {
// The `test` command serves in the exact same way, but it also sets `PERSEUS_TESTING`
// This will be used by the subcrates
env::set_var("PERSEUS_TESTING", "true");
// Set up the '.perseus/' directory if needed
prepare(dir.clone())?;
// Delete old build artifacts if `--no-build` wasn't specified
if !prog_args.contains(&"--no-build".to_string()) {
delete_artifacts(dir.clone(), "static")?;
}
let exit_code = serve(dir, &prog_args)?;
Ok(exit_code)
} else if prog_args[0] == "prep" {
// This command is deliberately undocumented, it's only used for testing
// Set up the '.perseus/' directory if needed
prepare(dir)?;
Ok(0)
} else if prog_args[0] == "eject" {
// Set up the '.perseus/' directory if needed
prepare(dir.clone())?;
eject(dir)?;
Ok(0)
} else if prog_args[0] == "clean" {
if prog_args.get(1) == Some(&"--dist".to_string()) {
// The user only wants to remove distribution artifacts
// We don't delete `render_conf.json` because it's literally impossible for that to be the source of a problem right now
delete_artifacts(dir.clone(), "static")?;
delete_artifacts(dir, "pkg")?;
} else {
// This command deletes the `.perseus/` directory completely, which musn't happen if the user has ejected
if has_ejected(dir.clone()) && prog_args.get(1) != Some(&"--force".to_string())
{
return Err(EjectionError::CleanAfterEject.into());
}
// Just delete the '.perseus/' directory directly, as we'd do in a corruption
delete_bad_dir(dir)?;
}

Ok(0)
} else {
writeln!(
stdout,
"Unknown command '{}'. You can see the help page with -h/--help.",
prog_args[0]
)
.expect("Failed to write to stdout.");
Ok(1)
// This command deletes the `.perseus/` directory completely, which musn't happen if the user has ejected
if has_ejected(dir.clone()) && !clean_opts.force {
return Err(EjectionError::CleanAfterEject.into());
}
// Just delete the '.perseus/' directory directly, as we'd do in a corruption
delete_bad_dir(dir)?;
}
0
}
} else {
writeln!(
stdout,
"Please provide a command to run, or use -h/--help to see the help page."
)
.expect("Failed to write to stdout.");
Ok(1)
}
Subcommand::Eject => {
eject(dir)?;
0
}
Subcommand::Prep => {
// The `.perseus/` directory has already been set up in the preliminaries, so we don't need to do anything here
0
}
};
Ok(exit_code)
}
3 changes: 2 additions & 1 deletion packages/perseus-cli/src/build.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::cmd::{cfg_spinner, run_stage};
use crate::errors::*;
use crate::parse::BuildOpts;
use crate::thread::{spawn_thread, ThreadHandle};
use console::{style, Emoji};
use indicatif::{MultiProgress, ProgressBar};
Expand Down Expand Up @@ -107,7 +108,7 @@ pub fn build_internal(
}

/// Builds the subcrates to get a directory that we can serve. Returns an exit code.
pub fn build(dir: PathBuf, _prog_args: &[String]) -> Result<i32, ExecutionError> {
pub fn build(dir: PathBuf, _opts: BuildOpts) -> Result<i32, ExecutionError> {
let spinners = MultiProgress::new();

let (sg_thread, wb_thread) = build_internal(dir.clone(), &spinners, 2)?;
Expand Down
10 changes: 0 additions & 10 deletions packages/perseus-cli/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,3 @@ pub enum ExportError {
#[error(transparent)]
ExecutionError(#[from] ExecutionError),
}

/// Reports the given error to the user with `anyhow`.
#[macro_export]
macro_rules! report_err {
($err:expr) => {
let err = ::anyhow::anyhow!($err);
// This will include a `Caused by` section
eprintln!("Error: {:?}", err);
};
}
3 changes: 2 additions & 1 deletion packages/perseus-cli/src/export.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::cmd::{cfg_spinner, run_stage};
use crate::errors::*;
use crate::parse::ExportOpts;
use crate::thread::{spawn_thread, ThreadHandle};
use console::{style, Emoji};
use indicatif::{MultiProgress, ProgressBar};
Expand Down Expand Up @@ -191,7 +192,7 @@ pub fn export_internal(
}

/// Builds the subcrates to get a directory that we can serve. Returns an exit code.
pub fn export(dir: PathBuf, _prog_args: &[String]) -> Result<i32, ExportError> {
pub fn export(dir: PathBuf, _opts: ExportOpts) -> Result<i32, ExportError> {
let spinners = MultiProgress::new();

let (ep_thread, wb_thread) = export_internal(dir.clone(), &spinners, 2)?;
Expand Down
27 changes: 0 additions & 27 deletions packages/perseus-cli/src/help.rs

This file was deleted.

4 changes: 2 additions & 2 deletions packages/perseus-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ mod cmd;
mod eject;
pub mod errors;
mod export;
mod help;
/// Parsing utilities for arguments.
pub mod parse;
mod prepare;
mod serve;
mod thread;
Expand All @@ -48,7 +49,6 @@ pub const PERSEUS_VERSION: &str = env!("CARGO_PKG_VERSION");
pub use build::build;
pub use eject::{eject, has_ejected};
pub use export::export;
pub use help::help;
pub use prepare::{check_env, prepare};
pub use serve::serve;

Expand Down
66 changes: 66 additions & 0 deletions packages/perseus-cli/src/parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#![allow(missing_docs)] // Prevents double-documenting some things

use crate::PERSEUS_VERSION;
use clap::{AppSettings, Clap};

// The documentation for the `Opts` struct will appear in the help page, hence the lack of puncutation and the lowercasing in places

/// The command-line interface for Perseus, a super-fast WebAssembly frontend development framework!
#[derive(Clap)]
#[clap(version = PERSEUS_VERSION)]
#[clap(setting = AppSettings::ColoredHelp)]
pub struct Opts {
#[clap(subcommand)]
pub subcmd: Subcommand,
}

#[derive(Clap)]
pub enum Subcommand {
Build(BuildOpts),
Export(ExportOpts),
Serve(ServeOpts),
/// Serves your app as `perseus serve` does, but puts it in testing mode
Test(ServeOpts),
Clean(CleanOpts),
/// Ejects you from the CLI harness, enabling you to work with the internals of Perseus
Eject,
/// Prepares the `.perseus/` directory (done automatically by `build` and `serve`)
Prep,
}
/// Builds your app
#[derive(Clap)]
pub struct BuildOpts {
/// Build for production
#[clap(long)]
release: bool,
}
/// Exports your app to purely static files
#[derive(Clap)]
pub struct ExportOpts {
/// Export for production
#[clap(long)]
release: bool,
}
/// Serves your app (set the `$HOST` and `$PORT` environment variables to change the location it's served at)
#[derive(Clap)]
pub struct ServeOpts {
/// Don't run the final binary, but print its location instead as the last line of output
#[clap(long)]
pub no_run: bool,
/// Only build the server, and use the results of a previous `perseus build`
#[clap(long)]
pub no_build: bool,
/// Build and serve for production
#[clap(long)]
release: bool,
}
/// Removes `.perseus/` entirely for updates or to fix corruptions
#[derive(Clap)]
pub struct CleanOpts {
/// Only remove the `.perseus/dist/` folder (use if you've ejected)
#[clap(short, long)]
pub dist: bool,
/// Remove the directory, even if you've ejected (this will permanently destroy any changes you've made to `.perseus/`!)
#[clap(short, long)]
pub force: bool,
}
Loading

0 comments on commit 83e365c

Please sign in to comment.