Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor CLI to use clap #34

Merged
merged 2 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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