Skip to content

Commit

Permalink
feat(cli): added basic hot reloading
Browse files Browse the repository at this point in the history
This doesn't work properly yet because old builds aren't terminated. The
server build never technically terminates because it then starts running
the server, so this is far from production-ready!
  • Loading branch information
arctic-hen7 committed Jan 1, 2022
1 parent efcf16f commit b4c93f0
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 12 deletions.
3 changes: 2 additions & 1 deletion packages/perseus-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ serde = "1"
serde_json = "1"
clap = { version = "=3.0.0-beta.5", features = ["color"] }
fs_extra = "1"
tokio = { version = "1", features = [ "macros", "rt-multi-thread" ] }
tokio = { version = "1", features = [ "macros", "rt-multi-thread", "sync" ] }
warp = "0.3"
notify = "4"

[lib]
name = "perseus_cli"
Expand Down
144 changes: 135 additions & 9 deletions packages/perseus-cli/src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use clap::Parser;
use fmterr::fmt_err;
use notify::{watcher, RecursiveMode, Watcher};
use perseus_cli::parse::SnoopSubcommand;
use perseus_cli::{
build, check_env, delete_artifacts, delete_bad_dir, deploy, eject, export, has_ejected,
Expand All @@ -10,6 +11,8 @@ use perseus_cli::{errors::*, snoop_build, snoop_server, snoop_wasm_build};
use std::env;
use std::io::Write;
use std::path::PathBuf;
use std::sync::mpsc::channel;
use std::time::Duration;

// All this does is run the program and terminate with the acquired exit code
#[tokio::main]
Expand All @@ -24,6 +27,8 @@ async fn main() {
std::process::exit(exit_code)
}

// IDEA Watch files at the `core()` level and then panic, catching the unwind in the watcher loop

// This manages error handling and returns a definite exit code to terminate with
async fn real_main() -> i32 {
// Get the working directory
Expand Down Expand Up @@ -86,27 +91,148 @@ async fn core(dir: PathBuf) -> Result<i32, Error> {
build(dir, build_opts)?
}
Subcommand::Export(export_opts) => {
// Delete old build/exportation artifacts
// Delete old build/export artifacts
delete_artifacts(dir.clone(), "static")?;
delete_artifacts(dir.clone(), "exported")?;
let exit_code = export(dir.clone(), export_opts.clone())?;
if exit_code != 0 {
return Ok(exit_code);
}
// Start a server for those files if requested
if export_opts.serve {
serve_exported(dir, export_opts.host, export_opts.port).await;
}

0
if export_opts.watch {
let dir_2 = dir.clone();
let export_opts_2 = export_opts.clone();
if export_opts.serve {
tokio::spawn(async move {
serve_exported(dir_2, export_opts_2.host, export_opts_2.port).await
});
}
// Now watch for changes
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(2))
.map_err(|err| WatchError::WatcherSetupFailed { source: err })?;
// Watch the current directory
for entry in std::fs::read_dir(".")
.map_err(|err| WatchError::ReadCurrentDirFailed { source: err })?
{
// We want to exclude `target/` and `.perseus/`, otherwise we should watch everything
let entry =
entry.map_err(|err| WatchError::ReadDirEntryFailed { source: err })?;
let name = entry.file_name();
if name != "target" && name != ".perseus" {
watcher
.watch(entry.path(), RecursiveMode::Recursive)
.map_err(|err| WatchError::WatchFileFailed {
filename: entry.path().to_str().unwrap().to_string(),
source: err,
})?;
}
}

let res: Result<i32, Error> = loop {
match rx.recv() {
Ok(_) => {
// Delete old build/exportation artifacts
delete_artifacts(dir.clone(), "static")?;
delete_artifacts(dir.clone(), "exported")?;
let dir_2 = dir.clone();
let opts = export_opts.clone();
match export(dir_2.clone(), opts.clone()) {
// We'l let the user know if there's a non-zero exit code
Ok(exit_code) => {
if exit_code != 0 {
eprintln!("Non-zero exit code returned from exporting process: {}.", exit_code)
}
}
// Because we're watching for changes, we can manage errors here
// We won't actually terminate unless the user tells us to
Err(err) => eprintln!("{}", fmt_err(&err)),
}
// TODO Reload the browser automatically
}
Err(err) => break Err(WatchError::WatcherError { source: err }.into()),
}
};
return res;
} else {
if export_opts.serve {
serve_exported(dir, export_opts.host, export_opts.port).await;
}
0
}
}
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, _server_path) = serve(dir, serve_opts)?;
exit_code
if serve_opts.watch {
match serve(dir.clone(), serve_opts.clone()) {
// We'll let the user know if there's a non-zero exit code
Ok((exit_code, _server_path)) => {
if exit_code != 0 {
eprintln!(
"Non-zero exit code returned from serving process: {}.",
exit_code
)
}
}
// Because we're watching for changes, we can manage errors here
// We won't actually terminate unless the user tells us to
Err(err) => eprintln!("{}", fmt_err(&err)),
};
// Now watch for changes
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(2))
.map_err(|err| WatchError::WatcherSetupFailed { source: err })?;
// Watch the current directory
for entry in std::fs::read_dir(".")
.map_err(|err| WatchError::ReadCurrentDirFailed { source: err })?
{
// We want to exclude `target/` and `.perseus/`, otherwise we should watch everything
let entry =
entry.map_err(|err| WatchError::ReadDirEntryFailed { source: err })?;
let name = entry.file_name();
if name != "target" && name != ".perseus" {
watcher
.watch(entry.path(), RecursiveMode::Recursive)
.map_err(|err| WatchError::WatchFileFailed {
filename: entry.path().to_str().unwrap().to_string(),
source: err,
})?;
}
}

let res: Result<i32, Error> = loop {
match rx.recv() {
Ok(_) => {
// Delete old build artifacts if `--no-build` wasn't specified
if !serve_opts.no_build {
delete_artifacts(dir.clone(), "static")?;
}
match serve(dir.clone(), serve_opts.clone()) {
// We'll let the user know if there's a non-zero exit code
Ok((exit_code, _server_path)) => {
if exit_code != 0 {
eprintln!(
"Non-zero exit code returned from serving process: {}.",
exit_code
)
}
}
// Because we're watching for changes, we can manage errors here
// We won't actually terminate unless the user tells us to
Err(err) => eprintln!("{}", fmt_err(&err)),
};
// TODO Reload the browser automatically
}
Err(err) => break Err(WatchError::WatcherError { source: err }.into()),
}
};
return res;
} else {
let (exit_code, _server_path) = serve(dir, serve_opts)?;
exit_code
}
}
Subcommand::Test(test_opts) => {
// This will be used by the subcrates
Expand Down
2 changes: 2 additions & 0 deletions packages/perseus-cli/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ fn deploy_full(dir: PathBuf, output: String, integration: Integration) -> Result
release: true,
standalone: true,
integration,
watch: false,
},
)?;
if serve_exit_code != 0 {
Expand Down Expand Up @@ -135,6 +136,7 @@ fn deploy_export(dir: PathBuf, output: String) -> Result<i32, Error> {
serve: false,
host: String::new(),
port: 0,
watch: false,
},
)?;
if export_exit_code != 0 {
Expand Down
32 changes: 32 additions & 0 deletions packages/perseus-cli/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub enum Error {
ExportError(#[from] ExportError),
#[error(transparent)]
DeployError(#[from] DeployError),
#[error(transparent)]
WatchError(#[from] WatchError),
}

/// Errors that can occur while preparing.
Expand Down Expand Up @@ -197,3 +199,33 @@ pub enum DeployError {
source: std::io::Error,
},
}

#[derive(Error, Debug)]
pub enum WatchError {
#[error("couldn't set up a file watcher, try re-running this command")]
WatcherSetupFailed {
#[source]
source: notify::Error,
},
#[error("couldn't read your current directory to watch files, do you have the necessary permissions?")]
ReadCurrentDirFailed {
#[source]
source: std::io::Error,
},
#[error("couldn't read entry in your current directory, try re-running this command")]
ReadDirEntryFailed {
#[source]
source: std::io::Error,
},
#[error("couldn't watch file at '{filename}', try re-running the command")]
WatchFileFailed {
filename: String,
#[source]
source: notify::Error,
},
#[error("an error occurred while watching files")]
WatcherError {
#[source]
source: std::sync::mpsc::RecvError,
},
}
10 changes: 8 additions & 2 deletions packages/perseus-cli/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct Opts {
pub subcmd: Subcommand,
}

#[derive(Parser, PartialEq, Eq)]
#[derive(Parser, PartialEq, Eq, Clone)]
pub enum Integration {
ActixWeb,
Warp,
Expand Down Expand Up @@ -80,9 +80,12 @@ pub struct ExportOpts {
/// The port to host your exported app on
#[clap(long, default_value = "8080")]
pub port: u16,
/// Whether or not to watch the files in your working directory for changes (exluding `target/` and `.perseus/`)
#[clap(short, long)]
pub watch: bool,
}
/// Serves your app (set the `$HOST` and `$PORT` environment variables to change the location it's served at)
#[derive(Parser)]
#[derive(Parser, Clone)]
pub struct ServeOpts {
/// Don't run the final binary, but print its location instead as the last line of output
#[clap(long)]
Expand All @@ -99,6 +102,9 @@ pub struct ServeOpts {
/// The server integration to use
#[clap(short, long, default_value = "warp")]
pub integration: Integration,
/// Whether or not to watch the files in your working directory for changes (exluding `target/` and `.perseus/`)
#[clap(short, long)]
pub watch: bool,
}
/// Removes `.perseus/` entirely for updates or to fix corruptions
#[derive(Parser)]
Expand Down

0 comments on commit b4c93f0

Please sign in to comment.