Skip to content

Commit 83e365c

Browse files
authored
refactor(cli): ♻️ migrated cli to clap (#34)
* refactor(cli): ♻️ migrated cli to `clap` * fix(cli): 🐛 fixed cli cleaning and switched to `fmterr`
1 parent 53bb61e commit 83e365c

File tree

9 files changed

+151
-141
lines changed

9 files changed

+151
-141
lines changed

packages/perseus-cli/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ include = [
2121
[dependencies]
2222
include_dir = "0.6"
2323
thiserror = "1"
24-
anyhow = "1"
24+
fmterr = "0.1"
2525
cargo_toml = "0.9"
2626
indicatif = "0.17.0-beta.1" # Not stable, but otherwise error handling is just about impossible
2727
console = "0.14"
2828
serde = "1"
2929
serde_json = "1"
30+
clap = "3.0.0-beta.4"
3031

3132
[lib]
3233
name = "perseus_cli"

packages/perseus-cli/src/bin/main.rs

+73-96
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
use clap::Clap;
2+
use fmterr::fmt_err;
13
use perseus_cli::errors::*;
24
use perseus_cli::{
3-
build, check_env, delete_artifacts, delete_bad_dir, eject, export, has_ejected, help, prepare,
4-
report_err, serve, PERSEUS_VERSION,
5+
build, check_env, delete_artifacts, delete_bad_dir, eject, export, has_ejected,
6+
parse::{Opts, Subcommand},
7+
prepare, serve,
58
};
69
use std::env;
710
use std::io::Write;
@@ -26,7 +29,10 @@ fn real_main() -> i32 {
2629
let dir = match dir {
2730
Ok(dir) => dir,
2831
Err(err) => {
29-
report_err!(PrepError::CurrentDirUnavailable { source: err });
32+
eprintln!(
33+
"{}",
34+
fmt_err(&PrepError::CurrentDirUnavailable { source: err })
35+
);
3036
return 1;
3137
}
3238
};
@@ -37,11 +43,11 @@ fn real_main() -> i32 {
3743
// If something failed, we print the error to `stderr` and return a failure exit code
3844
Err(err) => {
3945
let should_cause_deletion = err_should_cause_deletion(&err);
40-
report_err!(err);
46+
eprintln!("{}", fmt_err(&err));
4147
// Check if the error needs us to delete a partially-formed '.perseus/' directory
4248
if should_cause_deletion {
4349
if let Err(err) = delete_bad_dir(dir) {
44-
report_err!(err);
50+
eprintln!("{}", fmt_err(&err));
4551
}
4652
}
4753
1
@@ -56,105 +62,76 @@ fn real_main() -> i32 {
5662
fn core(dir: PathBuf) -> Result<i32, Error> {
5763
// Get `stdout` so we can write warnings appropriately
5864
let stdout = &mut std::io::stdout();
59-
// Get the arguments to this program, removing the first one (something like `perseus`)
60-
let mut prog_args: Vec<String> = env::args().collect();
61-
// This will panic if the first argument is not found (which is probably someone trying to fuzz us)
62-
let _executable_name = prog_args.remove(0);
63-
// Check the user's environment to make sure they have prerequisites
64-
check_env()?;
65+
6566
// Warn the user if they're using the CLI single-threaded mode
6667
if env::var("PERSEUS_CLI_SEQUENTIAL").is_ok() {
6768
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.");
6869
}
69-
// Check for special arguments
70-
if matches!(prog_args.get(0), Some(_)) {
71-
if prog_args[0] == "-v" || prog_args[0] == "--version" {
72-
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.");
73-
Ok(0)
74-
} else if prog_args[0] == "-h" || prog_args[0] == "--help" {
75-
help(stdout);
76-
Ok(0)
77-
} else {
78-
// Now we can check commands
79-
if prog_args[0] == "build" {
80-
// Set up the '.perseus/' directory if needed
81-
prepare(dir.clone())?;
82-
// Delete old build artifacts
70+
71+
// Parse the CLI options with `clap`
72+
let opts: Opts = Opts::parse();
73+
// Check the user's environment to make sure they have prerequisites
74+
// We do this after any help pages or version numbers have been parsed for snappiness
75+
check_env()?;
76+
// If we're not cleaning up artifacts, create them if needed
77+
if !matches!(opts.subcmd, Subcommand::Clean(_)) {
78+
prepare(dir.clone())?;
79+
}
80+
let exit_code = match opts.subcmd {
81+
Subcommand::Build(build_opts) => {
82+
// Delete old build artifacts
83+
delete_artifacts(dir.clone(), "static")?;
84+
build(dir, build_opts)?
85+
}
86+
Subcommand::Export(export_opts) => {
87+
// Delete old build/exportation artifacts
88+
delete_artifacts(dir.clone(), "static")?;
89+
delete_artifacts(dir.clone(), "exported")?;
90+
export(dir, export_opts)?
91+
}
92+
Subcommand::Serve(serve_opts) => {
93+
// Delete old build artifacts if `--no-build` wasn't specified
94+
if !serve_opts.no_build {
8395
delete_artifacts(dir.clone(), "static")?;
84-
let exit_code = build(dir, &prog_args)?;
85-
Ok(exit_code)
86-
} else if prog_args[0] == "export" {
87-
// Set up the '.perseus/' directory if needed
88-
prepare(dir.clone())?;
89-
// Delete old build/exportation artifacts
96+
}
97+
serve(dir, serve_opts)?
98+
}
99+
Subcommand::Test(test_opts) => {
100+
// This will be used by the subcrates
101+
env::set_var("PERSEUS_TESTING", "true");
102+
// Set up the '.perseus/' directory if needed
103+
prepare(dir.clone())?;
104+
// Delete old build artifacts if `--no-build` wasn't specified
105+
if !test_opts.no_build {
90106
delete_artifacts(dir.clone(), "static")?;
107+
}
108+
serve(dir, test_opts)?
109+
}
110+
Subcommand::Clean(clean_opts) => {
111+
if clean_opts.dist {
112+
// The user only wants to remove distribution artifacts
113+
// We don't delete `render_conf.json` because it's literally impossible for that to be the source of a problem right now
114+
delete_artifacts(dir.clone(), "static")?;
115+
delete_artifacts(dir.clone(), "pkg")?;
91116
delete_artifacts(dir.clone(), "exported")?;
92-
let exit_code = export(dir, &prog_args)?;
93-
Ok(exit_code)
94-
} else if prog_args[0] == "serve" {
95-
// Set up the '.perseus/' directory if needed
96-
prepare(dir.clone())?;
97-
// Delete old build artifacts if `--no-build` wasn't specified
98-
if !prog_args.contains(&"--no-build".to_string()) {
99-
delete_artifacts(dir.clone(), "static")?;
100-
}
101-
let exit_code = serve(dir, &prog_args)?;
102-
Ok(exit_code)
103-
} else if prog_args[0] == "test" {
104-
// The `test` command serves in the exact same way, but it also sets `PERSEUS_TESTING`
105-
// This will be used by the subcrates
106-
env::set_var("PERSEUS_TESTING", "true");
107-
// Set up the '.perseus/' directory if needed
108-
prepare(dir.clone())?;
109-
// Delete old build artifacts if `--no-build` wasn't specified
110-
if !prog_args.contains(&"--no-build".to_string()) {
111-
delete_artifacts(dir.clone(), "static")?;
112-
}
113-
let exit_code = serve(dir, &prog_args)?;
114-
Ok(exit_code)
115-
} else if prog_args[0] == "prep" {
116-
// This command is deliberately undocumented, it's only used for testing
117-
// Set up the '.perseus/' directory if needed
118-
prepare(dir)?;
119-
Ok(0)
120-
} else if prog_args[0] == "eject" {
121-
// Set up the '.perseus/' directory if needed
122-
prepare(dir.clone())?;
123-
eject(dir)?;
124-
Ok(0)
125-
} else if prog_args[0] == "clean" {
126-
if prog_args.get(1) == Some(&"--dist".to_string()) {
127-
// The user only wants to remove distribution artifacts
128-
// We don't delete `render_conf.json` because it's literally impossible for that to be the source of a problem right now
129-
delete_artifacts(dir.clone(), "static")?;
130-
delete_artifacts(dir, "pkg")?;
131-
} else {
132-
// This command deletes the `.perseus/` directory completely, which musn't happen if the user has ejected
133-
if has_ejected(dir.clone()) && prog_args.get(1) != Some(&"--force".to_string())
134-
{
135-
return Err(EjectionError::CleanAfterEject.into());
136-
}
137-
// Just delete the '.perseus/' directory directly, as we'd do in a corruption
138-
delete_bad_dir(dir)?;
139-
}
140-
141-
Ok(0)
142117
} else {
143-
writeln!(
144-
stdout,
145-
"Unknown command '{}'. You can see the help page with -h/--help.",
146-
prog_args[0]
147-
)
148-
.expect("Failed to write to stdout.");
149-
Ok(1)
118+
// This command deletes the `.perseus/` directory completely, which musn't happen if the user has ejected
119+
if has_ejected(dir.clone()) && !clean_opts.force {
120+
return Err(EjectionError::CleanAfterEject.into());
121+
}
122+
// Just delete the '.perseus/' directory directly, as we'd do in a corruption
123+
delete_bad_dir(dir)?;
150124
}
125+
0
151126
}
152-
} else {
153-
writeln!(
154-
stdout,
155-
"Please provide a command to run, or use -h/--help to see the help page."
156-
)
157-
.expect("Failed to write to stdout.");
158-
Ok(1)
159-
}
127+
Subcommand::Eject => {
128+
eject(dir)?;
129+
0
130+
}
131+
Subcommand::Prep => {
132+
// The `.perseus/` directory has already been set up in the preliminaries, so we don't need to do anything here
133+
0
134+
}
135+
};
136+
Ok(exit_code)
160137
}

packages/perseus-cli/src/build.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::cmd::{cfg_spinner, run_stage};
22
use crate::errors::*;
3+
use crate::parse::BuildOpts;
34
use crate::thread::{spawn_thread, ThreadHandle};
45
use console::{style, Emoji};
56
use indicatif::{MultiProgress, ProgressBar};
@@ -107,7 +108,7 @@ pub fn build_internal(
107108
}
108109

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

113114
let (sg_thread, wb_thread) = build_internal(dir.clone(), &spinners, 2)?;

packages/perseus-cli/src/errors.rs

-10
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,3 @@ pub enum ExportError {
153153
#[error(transparent)]
154154
ExecutionError(#[from] ExecutionError),
155155
}
156-
157-
/// Reports the given error to the user with `anyhow`.
158-
#[macro_export]
159-
macro_rules! report_err {
160-
($err:expr) => {
161-
let err = ::anyhow::anyhow!($err);
162-
// This will include a `Caused by` section
163-
eprintln!("Error: {:?}", err);
164-
};
165-
}

packages/perseus-cli/src/export.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::cmd::{cfg_spinner, run_stage};
22
use crate::errors::*;
3+
use crate::parse::ExportOpts;
34
use crate::thread::{spawn_thread, ThreadHandle};
45
use console::{style, Emoji};
56
use indicatif::{MultiProgress, ProgressBar};
@@ -191,7 +192,7 @@ pub fn export_internal(
191192
}
192193

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

197198
let (ep_thread, wb_thread) = export_internal(dir.clone(), &spinners, 2)?;

packages/perseus-cli/src/help.rs

-27
This file was deleted.

packages/perseus-cli/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ mod cmd;
3232
mod eject;
3333
pub mod errors;
3434
mod export;
35-
mod help;
35+
/// Parsing utilities for arguments.
36+
pub mod parse;
3637
mod prepare;
3738
mod serve;
3839
mod thread;
@@ -48,7 +49,6 @@ pub const PERSEUS_VERSION: &str = env!("CARGO_PKG_VERSION");
4849
pub use build::build;
4950
pub use eject::{eject, has_ejected};
5051
pub use export::export;
51-
pub use help::help;
5252
pub use prepare::{check_env, prepare};
5353
pub use serve::serve;
5454

packages/perseus-cli/src/parse.rs

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#![allow(missing_docs)] // Prevents double-documenting some things
2+
3+
use crate::PERSEUS_VERSION;
4+
use clap::{AppSettings, Clap};
5+
6+
// The documentation for the `Opts` struct will appear in the help page, hence the lack of puncutation and the lowercasing in places
7+
8+
/// The command-line interface for Perseus, a super-fast WebAssembly frontend development framework!
9+
#[derive(Clap)]
10+
#[clap(version = PERSEUS_VERSION)]
11+
#[clap(setting = AppSettings::ColoredHelp)]
12+
pub struct Opts {
13+
#[clap(subcommand)]
14+
pub subcmd: Subcommand,
15+
}
16+
17+
#[derive(Clap)]
18+
pub enum Subcommand {
19+
Build(BuildOpts),
20+
Export(ExportOpts),
21+
Serve(ServeOpts),
22+
/// Serves your app as `perseus serve` does, but puts it in testing mode
23+
Test(ServeOpts),
24+
Clean(CleanOpts),
25+
/// Ejects you from the CLI harness, enabling you to work with the internals of Perseus
26+
Eject,
27+
/// Prepares the `.perseus/` directory (done automatically by `build` and `serve`)
28+
Prep,
29+
}
30+
/// Builds your app
31+
#[derive(Clap)]
32+
pub struct BuildOpts {
33+
/// Build for production
34+
#[clap(long)]
35+
release: bool,
36+
}
37+
/// Exports your app to purely static files
38+
#[derive(Clap)]
39+
pub struct ExportOpts {
40+
/// Export for production
41+
#[clap(long)]
42+
release: bool,
43+
}
44+
/// Serves your app (set the `$HOST` and `$PORT` environment variables to change the location it's served at)
45+
#[derive(Clap)]
46+
pub struct ServeOpts {
47+
/// Don't run the final binary, but print its location instead as the last line of output
48+
#[clap(long)]
49+
pub no_run: bool,
50+
/// Only build the server, and use the results of a previous `perseus build`
51+
#[clap(long)]
52+
pub no_build: bool,
53+
/// Build and serve for production
54+
#[clap(long)]
55+
release: bool,
56+
}
57+
/// Removes `.perseus/` entirely for updates or to fix corruptions
58+
#[derive(Clap)]
59+
pub struct CleanOpts {
60+
/// Only remove the `.perseus/dist/` folder (use if you've ejected)
61+
#[clap(short, long)]
62+
pub dist: bool,
63+
/// Remove the directory, even if you've ejected (this will permanently destroy any changes you've made to `.perseus/`!)
64+
#[clap(short, long)]
65+
pub force: bool,
66+
}

0 commit comments

Comments
 (0)