Skip to content

Commit b4c93f0

Browse files
committed
feat(cli): added basic hot reloading
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!
1 parent efcf16f commit b4c93f0

File tree

5 files changed

+179
-12
lines changed

5 files changed

+179
-12
lines changed

packages/perseus-cli/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ serde = "1"
2929
serde_json = "1"
3030
clap = { version = "=3.0.0-beta.5", features = ["color"] }
3131
fs_extra = "1"
32-
tokio = { version = "1", features = [ "macros", "rt-multi-thread" ] }
32+
tokio = { version = "1", features = [ "macros", "rt-multi-thread", "sync" ] }
3333
warp = "0.3"
34+
notify = "4"
3435

3536
[lib]
3637
name = "perseus_cli"

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

+135-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use clap::Parser;
22
use fmterr::fmt_err;
3+
use notify::{watcher, RecursiveMode, Watcher};
34
use perseus_cli::parse::SnoopSubcommand;
45
use perseus_cli::{
56
build, check_env, delete_artifacts, delete_bad_dir, deploy, eject, export, has_ejected,
@@ -10,6 +11,8 @@ use perseus_cli::{errors::*, snoop_build, snoop_server, snoop_wasm_build};
1011
use std::env;
1112
use std::io::Write;
1213
use std::path::PathBuf;
14+
use std::sync::mpsc::channel;
15+
use std::time::Duration;
1316

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

30+
// IDEA Watch files at the `core()` level and then panic, catching the unwind in the watcher loop
31+
2732
// This manages error handling and returns a definite exit code to terminate with
2833
async fn real_main() -> i32 {
2934
// Get the working directory
@@ -86,27 +91,148 @@ async fn core(dir: PathBuf) -> Result<i32, Error> {
8691
build(dir, build_opts)?
8792
}
8893
Subcommand::Export(export_opts) => {
89-
// Delete old build/exportation artifacts
94+
// Delete old build/export artifacts
9095
delete_artifacts(dir.clone(), "static")?;
9196
delete_artifacts(dir.clone(), "exported")?;
9297
let exit_code = export(dir.clone(), export_opts.clone())?;
9398
if exit_code != 0 {
9499
return Ok(exit_code);
95100
}
96-
// Start a server for those files if requested
97-
if export_opts.serve {
98-
serve_exported(dir, export_opts.host, export_opts.port).await;
99-
}
100101

101-
0
102+
if export_opts.watch {
103+
let dir_2 = dir.clone();
104+
let export_opts_2 = export_opts.clone();
105+
if export_opts.serve {
106+
tokio::spawn(async move {
107+
serve_exported(dir_2, export_opts_2.host, export_opts_2.port).await
108+
});
109+
}
110+
// Now watch for changes
111+
let (tx, rx) = channel();
112+
let mut watcher = watcher(tx, Duration::from_secs(2))
113+
.map_err(|err| WatchError::WatcherSetupFailed { source: err })?;
114+
// Watch the current directory
115+
for entry in std::fs::read_dir(".")
116+
.map_err(|err| WatchError::ReadCurrentDirFailed { source: err })?
117+
{
118+
// We want to exclude `target/` and `.perseus/`, otherwise we should watch everything
119+
let entry =
120+
entry.map_err(|err| WatchError::ReadDirEntryFailed { source: err })?;
121+
let name = entry.file_name();
122+
if name != "target" && name != ".perseus" {
123+
watcher
124+
.watch(entry.path(), RecursiveMode::Recursive)
125+
.map_err(|err| WatchError::WatchFileFailed {
126+
filename: entry.path().to_str().unwrap().to_string(),
127+
source: err,
128+
})?;
129+
}
130+
}
131+
132+
let res: Result<i32, Error> = loop {
133+
match rx.recv() {
134+
Ok(_) => {
135+
// Delete old build/exportation artifacts
136+
delete_artifacts(dir.clone(), "static")?;
137+
delete_artifacts(dir.clone(), "exported")?;
138+
let dir_2 = dir.clone();
139+
let opts = export_opts.clone();
140+
match export(dir_2.clone(), opts.clone()) {
141+
// We'l let the user know if there's a non-zero exit code
142+
Ok(exit_code) => {
143+
if exit_code != 0 {
144+
eprintln!("Non-zero exit code returned from exporting process: {}.", exit_code)
145+
}
146+
}
147+
// Because we're watching for changes, we can manage errors here
148+
// We won't actually terminate unless the user tells us to
149+
Err(err) => eprintln!("{}", fmt_err(&err)),
150+
}
151+
// TODO Reload the browser automatically
152+
}
153+
Err(err) => break Err(WatchError::WatcherError { source: err }.into()),
154+
}
155+
};
156+
return res;
157+
} else {
158+
if export_opts.serve {
159+
serve_exported(dir, export_opts.host, export_opts.port).await;
160+
}
161+
0
162+
}
102163
}
103164
Subcommand::Serve(serve_opts) => {
104-
// Delete old build artifacts if `--no-build` wasn't specified
105165
if !serve_opts.no_build {
106166
delete_artifacts(dir.clone(), "static")?;
107167
}
108-
let (exit_code, _server_path) = serve(dir, serve_opts)?;
109-
exit_code
168+
if serve_opts.watch {
169+
match serve(dir.clone(), serve_opts.clone()) {
170+
// We'll let the user know if there's a non-zero exit code
171+
Ok((exit_code, _server_path)) => {
172+
if exit_code != 0 {
173+
eprintln!(
174+
"Non-zero exit code returned from serving process: {}.",
175+
exit_code
176+
)
177+
}
178+
}
179+
// Because we're watching for changes, we can manage errors here
180+
// We won't actually terminate unless the user tells us to
181+
Err(err) => eprintln!("{}", fmt_err(&err)),
182+
};
183+
// Now watch for changes
184+
let (tx, rx) = channel();
185+
let mut watcher = watcher(tx, Duration::from_secs(2))
186+
.map_err(|err| WatchError::WatcherSetupFailed { source: err })?;
187+
// Watch the current directory
188+
for entry in std::fs::read_dir(".")
189+
.map_err(|err| WatchError::ReadCurrentDirFailed { source: err })?
190+
{
191+
// We want to exclude `target/` and `.perseus/`, otherwise we should watch everything
192+
let entry =
193+
entry.map_err(|err| WatchError::ReadDirEntryFailed { source: err })?;
194+
let name = entry.file_name();
195+
if name != "target" && name != ".perseus" {
196+
watcher
197+
.watch(entry.path(), RecursiveMode::Recursive)
198+
.map_err(|err| WatchError::WatchFileFailed {
199+
filename: entry.path().to_str().unwrap().to_string(),
200+
source: err,
201+
})?;
202+
}
203+
}
204+
205+
let res: Result<i32, Error> = loop {
206+
match rx.recv() {
207+
Ok(_) => {
208+
// Delete old build artifacts if `--no-build` wasn't specified
209+
if !serve_opts.no_build {
210+
delete_artifacts(dir.clone(), "static")?;
211+
}
212+
match serve(dir.clone(), serve_opts.clone()) {
213+
// We'll let the user know if there's a non-zero exit code
214+
Ok((exit_code, _server_path)) => {
215+
if exit_code != 0 {
216+
eprintln!(
217+
"Non-zero exit code returned from serving process: {}.",
218+
exit_code
219+
)
220+
}
221+
}
222+
// Because we're watching for changes, we can manage errors here
223+
// We won't actually terminate unless the user tells us to
224+
Err(err) => eprintln!("{}", fmt_err(&err)),
225+
};
226+
// TODO Reload the browser automatically
227+
}
228+
Err(err) => break Err(WatchError::WatcherError { source: err }.into()),
229+
}
230+
};
231+
return res;
232+
} else {
233+
let (exit_code, _server_path) = serve(dir, serve_opts)?;
234+
exit_code
235+
}
110236
}
111237
Subcommand::Test(test_opts) => {
112238
// This will be used by the subcrates

packages/perseus-cli/src/deploy.rs

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ fn deploy_full(dir: PathBuf, output: String, integration: Integration) -> Result
3434
release: true,
3535
standalone: true,
3636
integration,
37+
watch: false,
3738
},
3839
)?;
3940
if serve_exit_code != 0 {
@@ -135,6 +136,7 @@ fn deploy_export(dir: PathBuf, output: String) -> Result<i32, Error> {
135136
serve: false,
136137
host: String::new(),
137138
port: 0,
139+
watch: false,
138140
},
139141
)?;
140142
if export_exit_code != 0 {

packages/perseus-cli/src/errors.rs

+32
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ pub enum Error {
1515
ExportError(#[from] ExportError),
1616
#[error(transparent)]
1717
DeployError(#[from] DeployError),
18+
#[error(transparent)]
19+
WatchError(#[from] WatchError),
1820
}
1921

2022
/// Errors that can occur while preparing.
@@ -197,3 +199,33 @@ pub enum DeployError {
197199
source: std::io::Error,
198200
},
199201
}
202+
203+
#[derive(Error, Debug)]
204+
pub enum WatchError {
205+
#[error("couldn't set up a file watcher, try re-running this command")]
206+
WatcherSetupFailed {
207+
#[source]
208+
source: notify::Error,
209+
},
210+
#[error("couldn't read your current directory to watch files, do you have the necessary permissions?")]
211+
ReadCurrentDirFailed {
212+
#[source]
213+
source: std::io::Error,
214+
},
215+
#[error("couldn't read entry in your current directory, try re-running this command")]
216+
ReadDirEntryFailed {
217+
#[source]
218+
source: std::io::Error,
219+
},
220+
#[error("couldn't watch file at '{filename}', try re-running the command")]
221+
WatchFileFailed {
222+
filename: String,
223+
#[source]
224+
source: notify::Error,
225+
},
226+
#[error("an error occurred while watching files")]
227+
WatcherError {
228+
#[source]
229+
source: std::sync::mpsc::RecvError,
230+
},
231+
}

packages/perseus-cli/src/parse.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub struct Opts {
1414
pub subcmd: Subcommand,
1515
}
1616

17-
#[derive(Parser, PartialEq, Eq)]
17+
#[derive(Parser, PartialEq, Eq, Clone)]
1818
pub enum Integration {
1919
ActixWeb,
2020
Warp,
@@ -80,9 +80,12 @@ pub struct ExportOpts {
8080
/// The port to host your exported app on
8181
#[clap(long, default_value = "8080")]
8282
pub port: u16,
83+
/// Whether or not to watch the files in your working directory for changes (exluding `target/` and `.perseus/`)
84+
#[clap(short, long)]
85+
pub watch: bool,
8386
}
8487
/// Serves your app (set the `$HOST` and `$PORT` environment variables to change the location it's served at)
85-
#[derive(Parser)]
88+
#[derive(Parser, Clone)]
8689
pub struct ServeOpts {
8790
/// Don't run the final binary, but print its location instead as the last line of output
8891
#[clap(long)]
@@ -99,6 +102,9 @@ pub struct ServeOpts {
99102
/// The server integration to use
100103
#[clap(short, long, default_value = "warp")]
101104
pub integration: Integration,
105+
/// Whether or not to watch the files in your working directory for changes (exluding `target/` and `.perseus/`)
106+
#[clap(short, long)]
107+
pub watch: bool,
102108
}
103109
/// Removes `.perseus/` entirely for updates or to fix corruptions
104110
#[derive(Parser)]

0 commit comments

Comments
 (0)