diff --git a/packages/perseus-cli/Cargo.toml b/packages/perseus-cli/Cargo.toml index b3a6cca29a..e2d5cb8f19 100644 --- a/packages/perseus-cli/Cargo.toml +++ b/packages/perseus-cli/Cargo.toml @@ -9,6 +9,9 @@ edition = "2018" include_dir = "0.6" error-chain = "0.12" cargo_toml = "0.9" +indicatif = "0.16" +console = "0.14" +notify = "4.0" [lib] name = "lib" diff --git a/packages/perseus-cli/src/bin/main.rs b/packages/perseus-cli/src/bin/main.rs index d72aa532da..53e8d062c9 100644 --- a/packages/perseus-cli/src/bin/main.rs +++ b/packages/perseus-cli/src/bin/main.rs @@ -6,9 +6,9 @@ use lib::errors::*; // All this does is run the program and terminate with the acquired exit code fn main() { - // In development, we'll test in the basic example + // In development, we'll test in the `basic` example if cfg!(debug_assertions) { - env::set_current_dir("../../examples/basic").unwrap(); + env::set_current_dir("../../examples/cli").unwrap(); } let exit_code = real_main(); std::process::exit(exit_code) @@ -60,7 +60,7 @@ fn core(dir: PathBuf) -> Result { // 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 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); @@ -70,24 +70,24 @@ fn core(dir: PathBuf) -> Result { if prog_args[0] == "build" { // Set up the '.perseus/' directory if needed prepare(dir.clone())?; - build(dir, &prog_args)?; - Ok(0) + let exit_code = build(dir, &prog_args)?; + Ok(exit_code) } else if prog_args[0] == "serve" { // Set up the '.perseus/' directory if needed prepare(dir.clone())?; - serve(dir, &prog_args)?; - Ok(0) + let exit_code = serve(dir, &prog_args)?; + Ok(exit_code) } else if prog_args[0] == "clean" { // 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]); + writeln!(stdout, "Unknown command '{}'. You can see the help page with -h/--help.", prog_args[0]).expect("Failed to write to stdout."); Ok(1) } } } else { - writeln!(stdout, "Please provide a command to run, or use -h/--help to see the help page."); + 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) } } diff --git a/packages/perseus-cli/src/build.rs b/packages/perseus-cli/src/build.rs index 1edeaf09c1..d30a1e6fbb 100644 --- a/packages/perseus-cli/src/build.rs +++ b/packages/perseus-cli/src/build.rs @@ -1,7 +1,82 @@ use std::path::PathBuf; +use console::{style, Emoji}; +use crate::cmd::run_stage; use crate::errors::*; -/// Builds the subcrates to get a directory that we can serve. -pub fn build(dir: PathBuf, prog_args: &[String]) -> Result<()> { - todo!("build command") +// Emojis for stages +static GENERATING: Emoji<'_, '_> = Emoji("🔨", ""); +static BUILDING: Emoji<'_, '_> = Emoji("🏗️ ", ""); // Yes, there's a space here, for some reason it's needed... +static FINALIZING: Emoji<'_, '_> = Emoji("📦", ""); + +/// Returns the exit code if it's non-zero. +macro_rules! handle_exit_code { + ($code:expr) => { + let code = $code; + if code != 0 { + return Ok(code); + } + }; +} + +/// Actually builds the user's code, program arguments having been interpreted. +fn build_internal(dir: PathBuf) -> Result { + let mut target = dir; + target.extend([".perseus"]); + + // Static generation + handle_exit_code!(run_stage( + vec![ + "cargo run" + ], + &target, + format!( + "{} {} Generating your app", + style("[1/3]").bold().dim(), + GENERATING + ) + )?); + // WASM building + handle_exit_code!(run_stage( + vec![ + "wasm-pack build --target web", + // Move the `pkg/` directory into `dist/pkg/` + "rm -rf dist/pkg", + "mv pkg/ dist/", + ], + &target, + format!( + "{} {} Building your app to WASM", + style("[2/3]").bold().dim(), + BUILDING + ) + )?); + // JS bundle generation + handle_exit_code!(run_stage( + vec![ + "rollup main.js --format iife --file dist/pkg/bundle.js" + ], + &target, + format!( + "{} {} Finalizing bundle", + style("[3/3]").bold().dim(), + FINALIZING + ) + )?); + + Ok(0) +} + +/// Builds the subcrates to get a directory that we can serve. Returns an exit code. +pub fn build(dir: PathBuf, prog_args: &[String]) -> Result { + // TODO support watching files + // If we should watch for file changes, do so + let should_watch = prog_args.get(1); + let dflt_watch_path = ".".to_string(); + let _watch_path = prog_args.get(2).unwrap_or(&dflt_watch_path); + if should_watch == Some(&"-w".to_string()) || should_watch == Some(&"--watch".to_string()) { + todo!("watching not yet supported, try a tool like 'entr'"); + } + let exit_code = build_internal(dir.clone())?; + + Ok(exit_code) } diff --git a/packages/perseus-cli/src/cmd.rs b/packages/perseus-cli/src/cmd.rs new file mode 100644 index 0000000000..4732142f04 --- /dev/null +++ b/packages/perseus-cli/src/cmd.rs @@ -0,0 +1,75 @@ +use std::process::Command; +use std::path::Path; +use std::io::Write; +use indicatif::{ProgressBar, ProgressStyle}; +use console::Emoji; +use crate::errors::*; + +// Some useful emojis +static SUCCESS: Emoji<'_, '_> = Emoji("✅", "success!"); +static FAILURE: Emoji<'_, '_> = Emoji("❌", "failed!"); + +/// Runs the given command conveniently, returning the exit code. Notably, this parses the given command by separating it on spaces. +pub fn run_cmd(raw_cmd: String, dir: &Path, pre_dump: impl Fn()) -> Result { + let mut cmd_args: Vec<&str> = raw_cmd.split(' ').collect(); + let cmd = cmd_args.remove(0); + + // This will NOT pipe output/errors to the console + let output = Command::new(&cmd) + .args(cmd_args) + .current_dir(dir) + .output() + .map_err(|err| ErrorKind::CmdExecFailed(raw_cmd.clone(), err.to_string()))?; + + let exit_code = match output.status.code() { + Some(exit_code) => exit_code, // If we have an exit code, use it + None if output.status.success() => 0, // If we don't, but we know the command succeeded, return 0 (success code) + None => 1, // If we don't know an exit code but we know that the command failed, return 1 (general error code) + }; + + // Print `stderr` only if there's something therein and the exit code is non-zero + if !output.stderr.is_empty() && exit_code != 0 { + pre_dump(); + std::io::stderr().write_all(&output.stderr).unwrap(); + } + + Ok(exit_code) +} + +pub fn run_stage(cmds: Vec<&str>, target: &Path, message: String) -> Result { + // Tell the user about the stage with a nice progress bar + let spinner = ProgressBar::new_spinner(); + spinner.set_style( + ProgressStyle::default_spinner() + .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ") + ); + spinner.set_message(format!("{}...", message)); + // Tick the spinner every 50 milliseconds + spinner.enable_steady_tick(50); + + // Run the commands + for cmd in cmds { + // We make sure all commands run in the target directory ('.perseus/' itself) + let exit_code = run_cmd(cmd.to_string(), target, || { + // We're done, we'll write a more permanent version of the message + spinner.finish_with_message(format!( + "{}...{}", + message, + FAILURE + )) + })?; + // If we have a non-zero exit code, we should NOT continue (stderr has been written to the console already) + if exit_code != 0 { + return Ok(1); + } + } + + // We're done, we'll write a more permanent version of the message + spinner.finish_with_message(format!( + "{}...{}", + message, + SUCCESS + )); + + Ok(0) +} diff --git a/packages/perseus-cli/src/errors.rs b/packages/perseus-cli/src/errors.rs index 7e07eb06cc..f5766e1fd7 100644 --- a/packages/perseus-cli/src/errors.rs +++ b/packages/perseus-cli/src/errors.rs @@ -44,6 +44,16 @@ error_chain! { display("Couldn't remove '.perseus/' directory at '{:?}'. Please remove the '.perseus/' directory manually (particularly if you didn't intentionally run the 'clean' command, that means the directory has been corrupted). Error was: '{}'.", target, err) } + /// For when executing a system command after preparation failed. This shouldn't cause a directory deletion. + CmdExecFailed(cmd: String, err: String) { + description("command exeuction failed") + display("Couldn't execute command '{}'. Error was: '{}'.", cmd, err) + } + /// For when watching failes for changes failed. + WatcherFailed(path: String, err: String) { + description("watching files failed") + display("Couldn't watch '{}' for changes. Error was: '{}'.", path, err) + } } } diff --git a/packages/perseus-cli/src/help.rs b/packages/perseus-cli/src/help.rs index b23e3b4a96..2580d14455 100644 --- a/packages/perseus-cli/src/help.rs +++ b/packages/perseus-cli/src/help.rs @@ -11,12 +11,13 @@ This is the CLI for Perseus, a super-fast WebAssembly frontend development frame -h, --help prints this help page -v, --version prints the current version of the CLI -build builds your app (-p/--prod for production, -w/--watch to watch files) -serve serves your app (accepts $PORT and $HOST env vars) +build builds your app +serve serves your app (accepts $PORT and $HOST env vars, --no-build to serve pre-built files) +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.") -} \ No newline at end of file +} diff --git a/packages/perseus-cli/src/lib.rs b/packages/perseus-cli/src/lib.rs index 06de44d866..135912e278 100644 --- a/packages/perseus-cli/src/lib.rs +++ b/packages/perseus-cli/src/lib.rs @@ -3,6 +3,7 @@ mod prepare; pub mod errors; mod build; mod serve; +mod cmd; use errors::*; use std::fs; diff --git a/packages/perseus-cli/src/serve.rs b/packages/perseus-cli/src/serve.rs index 5f1fd40f73..65c95abc95 100644 --- a/packages/perseus-cli/src/serve.rs +++ b/packages/perseus-cli/src/serve.rs @@ -4,6 +4,6 @@ use crate::errors::*; /// Serves the user's app. If no arguments are provided, this will build in watch mode and serve. If `-p/--prod` is specified, we'll /// build for development, and if `--no-build` is specified, we won't build at all (useful for pseudo-production serving). /// General message though: do NOT use the CLI for production serving! -pub fn serve(dir: PathBuf, prog_args: &[String]) -> Result<()> { +pub fn serve(dir: PathBuf, prog_args: &[String]) -> Result { todo!("serve command") }