diff --git a/packages/perseus-cli/Cargo.toml b/packages/perseus-cli/Cargo.toml index 199898e6f3..aa4c99551c 100644 --- a/packages/perseus-cli/Cargo.toml +++ b/packages/perseus-cli/Cargo.toml @@ -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" diff --git a/packages/perseus-cli/src/bin/main.rs b/packages/perseus-cli/src/bin/main.rs index 556f8d5c05..e9260cc797 100644 --- a/packages/perseus-cli/src/bin/main.rs +++ b/packages/perseus-cli/src/bin/main.rs @@ -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; @@ -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; } }; @@ -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 @@ -56,105 +62,76 @@ fn real_main() -> i32 { fn core(dir: PathBuf) -> Result { // 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 = 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) } diff --git a/packages/perseus-cli/src/build.rs b/packages/perseus-cli/src/build.rs index 4a58e95636..2c4c484cec 100644 --- a/packages/perseus-cli/src/build.rs +++ b/packages/perseus-cli/src/build.rs @@ -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}; @@ -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 { +pub fn build(dir: PathBuf, _opts: BuildOpts) -> Result { let spinners = MultiProgress::new(); let (sg_thread, wb_thread) = build_internal(dir.clone(), &spinners, 2)?; diff --git a/packages/perseus-cli/src/errors.rs b/packages/perseus-cli/src/errors.rs index dd2e1283bd..6dfd58e30b 100644 --- a/packages/perseus-cli/src/errors.rs +++ b/packages/perseus-cli/src/errors.rs @@ -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); - }; -} diff --git a/packages/perseus-cli/src/export.rs b/packages/perseus-cli/src/export.rs index b764fbff3d..6d84bf9e41 100644 --- a/packages/perseus-cli/src/export.rs +++ b/packages/perseus-cli/src/export.rs @@ -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}; @@ -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 { +pub fn export(dir: PathBuf, _opts: ExportOpts) -> Result { let spinners = MultiProgress::new(); let (ep_thread, wb_thread) = export_internal(dir.clone(), &spinners, 2)?; diff --git a/packages/perseus-cli/src/help.rs b/packages/perseus-cli/src/help.rs deleted file mode 100644 index b8735cfd69..0000000000 --- a/packages/perseus-cli/src/help.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::PERSEUS_VERSION; - -/// Prints the help page. -pub fn help(output: &mut impl std::io::Write) { - writeln!( - output, - "Perseus v{version} help page: -------------------------- - -This is the CLI for Perseus, a super-fast WebAssembly frontend development framework! For the full reference, please see the documentation at https://arctic-hen7.github.io/perseus. - --h, --help prints this help page --v, --version prints the current version of the CLI - -build builds your app -serve serves your app (accepts $PORT and $HOST env vars, `--no-build` to serve pre-built files, `--no-run` to print server executable instead of running it) -test servers your app in testing mode (allows tests to be run against it) -clean removes `.perseus/` entirely (use `--dist` to only remove build artifacts) -eject ejects your app from the CLI harness, see documentation at https://arctic-hen7.github.io/perseus/cli/ejection.html - -Please note that watching for file changes is not yet inbuilt, but can be achieved with a tool like 'entr' in the meantime. -Further information can be found at https://arctic-hen7.github.io/perseus. - ", - version = PERSEUS_VERSION - ) - .expect("Failed to write help page.") -} diff --git a/packages/perseus-cli/src/lib.rs b/packages/perseus-cli/src/lib.rs index 73c4016775..d02652a245 100644 --- a/packages/perseus-cli/src/lib.rs +++ b/packages/perseus-cli/src/lib.rs @@ -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; @@ -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; diff --git a/packages/perseus-cli/src/parse.rs b/packages/perseus-cli/src/parse.rs new file mode 100644 index 0000000000..7ce028be79 --- /dev/null +++ b/packages/perseus-cli/src/parse.rs @@ -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, +} diff --git a/packages/perseus-cli/src/serve.rs b/packages/perseus-cli/src/serve.rs index 1d867254c6..338d46b2f3 100644 --- a/packages/perseus-cli/src/serve.rs +++ b/packages/perseus-cli/src/serve.rs @@ -1,6 +1,7 @@ use crate::build::{build_internal, finalize}; use crate::cmd::{cfg_spinner, run_stage}; use crate::errors::*; +use crate::parse::ServeOpts; use crate::thread::{spawn_thread, ThreadHandle}; use console::{style, Emoji}; use indicatif::{MultiProgress, ProgressBar}; @@ -173,11 +174,11 @@ fn run_server( } /// Builds the subcrates to get a directory that we can serve and then serves it. -pub fn serve(dir: PathBuf, prog_args: &[String]) -> Result { +pub fn serve(dir: PathBuf, opts: ServeOpts) -> Result { let spinners = MultiProgress::new(); // TODO support watching files - let did_build = !prog_args.contains(&"--no-build".to_string()); - let should_run = !prog_args.contains(&"--no-run".to_string()); + let did_build = !opts.no_build; + let should_run = !opts.no_run; // We need to have a way of knowing what the executable path to the server is let exec = Arc::new(Mutex::new(String::new())); // We can begin building the server in a thread without having to deal with the rest of the build stage yet