Skip to content

Commit 5a1be87

Browse files
committed
feat(cli): added perseus check for faster validation
This is basically `cargo check`, but it does both the engine-side and the browser-side simultaneously, allowing more assured and more rapid development. This is also lightning-fast, thanks to Cargo!
1 parent 8fd19c8 commit 5a1be87

File tree

5 files changed

+225
-3
lines changed

5 files changed

+225
-3
lines changed

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

+14-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ use command_group::stdlib::CommandGroup;
33
use directories::ProjectDirs;
44
use fmterr::fmt_err;
55
use notify::{recommended_watcher, RecursiveMode, Watcher};
6-
use perseus_cli::parse::{ExportOpts, ServeOpts, SnoopSubcommand};
6+
use perseus_cli::parse::{CheckOpts, ExportOpts, ServeOpts, SnoopSubcommand};
77
use perseus_cli::{
88
build, check_env, delete_artifacts, deploy, export, init, new,
99
parse::{Opts, Subcommand},
1010
serve, serve_exported, tinker,
1111
};
1212
use perseus_cli::{
13-
create_dist, delete_dist, errors::*, export_error_page, order_reload, run_reload_server,
13+
check, create_dist, delete_dist, errors::*, export_error_page, order_reload, run_reload_server,
1414
snoop_build, snoop_server, snoop_wasm_build, Tools,
1515
};
1616
use std::env;
@@ -121,6 +121,11 @@ async fn core(dir: PathBuf) -> Result<i32, Error> {
121121
watch,
122122
custom_watch,
123123
..
124+
})
125+
| Subcommand::Check(CheckOpts {
126+
watch,
127+
custom_watch,
128+
..
124129
}) if *watch && watch_allowed => {
125130
let (tx_term, rx) = channel();
126131
let tx_fs = tx_term.clone();
@@ -350,6 +355,13 @@ async fn core_watch(dir: PathBuf, opts: Opts) -> Result<i32, Error> {
350355
}
351356
Subcommand::New(ref new_opts) => new(dir, new_opts, &opts)?,
352357
Subcommand::Init(ref init_opts) => init(dir, init_opts)?,
358+
Subcommand::Check(ref check_opts) => {
359+
create_dist(&dir)?;
360+
let tools = Tools::new(&dir, &opts).await?;
361+
// Delete old build artifacts
362+
delete_artifacts(dir.clone(), "static")?;
363+
check(dir, check_opts, &tools, &opts)?
364+
}
353365
};
354366
Ok(exit_code)
355367
}

packages/perseus-cli/src/build.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use console::{style, Emoji};
77
use indicatif::{MultiProgress, ProgressBar};
88
use std::path::PathBuf;
99

10-
// Emojis for stages
10+
// Emoji for stages
1111
static GENERATING: Emoji<'_, '_> = Emoji("🔨", "");
1212
static BUILDING: Emoji<'_, '_> = Emoji("🏗️ ", ""); // Yes, there's a space here, for some reason it's needed...
1313

packages/perseus-cli/src/check.rs

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
use crate::{
2+
cmd::{cfg_spinner, run_stage},
3+
errors::*,
4+
parse::{CheckOpts, Opts},
5+
thread::{spawn_thread, ThreadHandle},
6+
Tools,
7+
};
8+
use console::{style, Emoji};
9+
use indicatif::{MultiProgress, ProgressBar};
10+
use std::path::PathBuf;
11+
12+
// Emoji for stages
13+
static CHECKING_ENGINE: Emoji<'_, '_> = Emoji("🦾", "");
14+
static CHECKING_BROWSER: Emoji<'_, '_> = Emoji("🌐", "");
15+
static GENERATING: Emoji<'_, '_> = Emoji("🔨", "");
16+
17+
/// Returns the exit code if it's non-zero.
18+
macro_rules! handle_exit_code {
19+
($code:expr) => {
20+
let (_, _, code) = $code;
21+
if code != 0 {
22+
return ::std::result::Result::Ok(code);
23+
}
24+
};
25+
}
26+
27+
/// Checks the user's app by checking their code and building it. This will
28+
/// first run `cargo check`, and then `cargo check --target
29+
/// wasm32-unknown-unknown`, so we can error quickly on compilation errors.
30+
/// If those both succeed, then we'll actually try to generate build artifacts,
31+
/// which is the only other place a Perseus can reasonably fail at build-time.
32+
pub fn check(
33+
dir: PathBuf,
34+
check_opts: &CheckOpts,
35+
tools: &Tools,
36+
global_opts: &Opts,
37+
) -> Result<i32, ExecutionError> {
38+
// First, run `cargo check`
39+
let spinners = MultiProgress::new();
40+
let num_steps = if check_opts.generate { 3 } else { 2 };
41+
42+
let (engine_thread, browser_thread) =
43+
cargo_check(dir.clone(), &spinners, num_steps, tools, global_opts)?;
44+
let engine_res = engine_thread
45+
.join()
46+
.map_err(|_| ExecutionError::ThreadWaitFailed)??;
47+
if engine_res != 0 {
48+
return Ok(engine_res);
49+
}
50+
let browser_res = browser_thread
51+
.join()
52+
.map_err(|_| ExecutionError::ThreadWaitFailed)??;
53+
if browser_res != 0 {
54+
return Ok(browser_res);
55+
}
56+
57+
// If that worked, generate static artifacts (if we've been told to)
58+
if check_opts.generate {
59+
let generation_res =
60+
run_static_generation(dir, &MultiProgress::new(), num_steps, tools, global_opts)?;
61+
Ok(generation_res)
62+
} else {
63+
Ok(0)
64+
}
65+
}
66+
67+
/// Runs `cargo check` for both the engine-side and the browser-side
68+
/// simultaneously, returning handles to the underlying threads. This will
69+
/// create progress bars as appropriate.
70+
#[allow(clippy::type_complexity)]
71+
fn cargo_check(
72+
dir: PathBuf,
73+
spinners: &MultiProgress,
74+
num_steps: u8,
75+
tools: &Tools,
76+
global_opts: &Opts,
77+
) -> Result<
78+
(
79+
ThreadHandle<impl FnOnce() -> Result<i32, ExecutionError>, Result<i32, ExecutionError>>,
80+
ThreadHandle<impl FnOnce() -> Result<i32, ExecutionError>, Result<i32, ExecutionError>>,
81+
),
82+
ExecutionError,
83+
> {
84+
// We need to own this for the threads
85+
let tools = tools.clone();
86+
let Opts {
87+
cargo_engine_args,
88+
cargo_browser_args,
89+
..
90+
} = global_opts.clone();
91+
92+
let engine_msg = format!(
93+
"{} {} Checking your app's engine-side",
94+
style(format!("[1/{}]", num_steps)).bold().dim(),
95+
CHECKING_ENGINE
96+
);
97+
let browser_msg = format!(
98+
"{} {} Checking your app's browser-side",
99+
style(format!("[2/{}]", num_steps)).bold().dim(),
100+
CHECKING_BROWSER
101+
);
102+
103+
// We parallelize the first two spinners
104+
let engine_spinner = spinners.insert(0, ProgressBar::new_spinner());
105+
let engine_spinner = cfg_spinner(engine_spinner, &engine_msg);
106+
let engine_dir = dir.clone();
107+
let browser_spinner = spinners.insert(1, ProgressBar::new_spinner());
108+
let browser_spinner = cfg_spinner(browser_spinner, &browser_msg);
109+
let browser_dir = dir;
110+
let cargo_engine_exec = tools.cargo_engine.clone();
111+
let engine_thread = spawn_thread(
112+
move || {
113+
handle_exit_code!(run_stage(
114+
vec![&format!(
115+
"{} check {}",
116+
cargo_engine_exec, cargo_engine_args
117+
)],
118+
&engine_dir,
119+
&engine_spinner,
120+
&engine_msg,
121+
vec![
122+
// We still need this for checking, because otherwise we can't chcek the engine
123+
// and the browser simultaneously (different targets, so no
124+
// commonalities gained by one directory)
125+
("CARGO_TARGET_DIR", "dist/target_engine")
126+
]
127+
)?);
128+
129+
Ok(0)
130+
},
131+
global_opts.sequential,
132+
);
133+
let browser_thread = spawn_thread(
134+
move || {
135+
handle_exit_code!(run_stage(
136+
vec![&format!(
137+
"{} check --target wasm32-unknown-unknown {}",
138+
tools.cargo_browser, cargo_browser_args
139+
)],
140+
&browser_dir,
141+
&browser_spinner,
142+
&browser_msg,
143+
vec![("CARGO_TARGET_DIR", "dist/target_wasm"),]
144+
)?);
145+
146+
Ok(0)
147+
},
148+
global_opts.sequential,
149+
);
150+
151+
Ok((engine_thread, browser_thread))
152+
}
153+
154+
#[allow(clippy::type_complexity)]
155+
fn run_static_generation(
156+
dir: PathBuf,
157+
spinners: &MultiProgress,
158+
num_steps: u8,
159+
tools: &Tools,
160+
global_opts: &Opts,
161+
) -> Result<i32, ExecutionError> {
162+
let Opts {
163+
cargo_engine_args, ..
164+
} = global_opts.clone();
165+
166+
let msg = format!(
167+
"{} {} Checking your app's page generation",
168+
style(format!("[3/{}]", num_steps)).bold().dim(),
169+
GENERATING
170+
);
171+
172+
// We parallelize the first two spinners
173+
let spinner = spinners.insert(0, ProgressBar::new_spinner());
174+
let spinner = cfg_spinner(spinner, &msg);
175+
176+
handle_exit_code!(run_stage(
177+
vec![&format!("{} run {}", tools.cargo_engine, cargo_engine_args)],
178+
&dir,
179+
&spinner,
180+
&msg,
181+
vec![
182+
("PERSEUS_ENGINE_OPERATION", "build"),
183+
("CARGO_TARGET_DIR", "dist/target_engine")
184+
]
185+
)?);
186+
187+
Ok(0)
188+
}

packages/perseus-cli/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ the documentation you'd like to see on this front!
1616
#![deny(missing_docs)]
1717

1818
mod build;
19+
mod check;
1920
mod cmd;
2021
mod deploy;
2122
pub mod errors;
@@ -40,6 +41,7 @@ use std::{fs, path::Path};
4041
/// The current version of the CLI, extracted from the crate version.
4142
pub const PERSEUS_VERSION: &str = env!("CARGO_PKG_VERSION");
4243
pub use build::build;
44+
pub use check::check;
4345
pub use deploy::deploy;
4446
pub use export::export;
4547
pub use export_error_page::export_error_page;

packages/perseus-cli/src/parse.rs

+20
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ pub enum Subcommand {
101101
Snoop(SnoopSubcommand),
102102
New(NewOpts),
103103
Init(InitOpts),
104+
/// Checks if your app builds properly for both the engine-side and the
105+
/// browser-side
106+
Check(CheckOpts),
104107
}
105108
/// Builds your app
106109
#[derive(Parser, Clone)]
@@ -238,3 +241,20 @@ pub struct SnoopServeOpts {
238241
#[clap(long, default_value = "8080")]
239242
pub port: u16,
240243
}
244+
245+
#[derive(Parser, Clone)]
246+
pub struct CheckOpts {
247+
/// Watch the files in your working directory for changes (exluding
248+
/// `target/` and `dist/`)
249+
#[clap(short, long)]
250+
pub watch: bool,
251+
/// Marks a specific file/directory to be watched (directories will be
252+
/// recursively watched)
253+
#[clap(long)]
254+
pub custom_watch: Vec<String>,
255+
/// Make sure the app's page generation works properly (this will take much
256+
/// longer, but almost guarantees that your app will actually build);
257+
/// use this to catch errors in build state and the like
258+
#[clap(short, long)]
259+
pub generate: bool,
260+
}

0 commit comments

Comments
 (0)