Skip to content

Commit

Permalink
feat(sozo): add sozo dev command (#2664)
Browse files Browse the repository at this point in the history
* feat: add sozo dev back

* fix: add debounce logic for better experience
  • Loading branch information
glihm authored Nov 8, 2024
1 parent c7900e9 commit 61febd5
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 48 deletions.
79 changes: 79 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bin/sozo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dojo-utils.workspace = true
dojo-world.workspace = true
itertools.workspace = true
katana-rpc-api.workspace = true
notify = "7.0.0"
tabled = { version = "0.16.0", features = [ "ansi" ] }
scarb.workspace = true
scarb-ui.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion bin/sozo/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use tracing::debug;

use crate::commands::check_package_dojo_version;

#[derive(Debug, Args)]
#[derive(Debug, Clone, Args)]
pub struct BuildArgs {
#[arg(long)]
#[arg(help = "Generate Typescript bindings.")]
Expand Down
145 changes: 104 additions & 41 deletions bin/sozo/src/commands/dev.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use std::sync::mpsc::channel;
use std::time::Duration;
use std::thread;
use std::time::{Duration, Instant};

use anyhow::Result;
use clap::Args;
use notify::event::Event;
use notify::{EventKind, PollWatcher, RecursiveMode, Watcher};
use scarb::core::Config;
use scarb_ui::args::{FeaturesSpec, PackagesFilter};
use tracing::{error, info, trace};

use super::build::BuildArgs;
use super::migrate::MigrateArgs;
use super::options::account::AccountOptions;
use super::options::starknet::StarknetOptions;
use super::options::transaction::TransactionOptions;
use super::options::world::WorldOptions;

#[derive(Debug, Args)]
Expand All @@ -24,80 +27,140 @@ pub struct DevArgs {

#[command(flatten)]
pub account: AccountOptions,

#[command(flatten)]
pub transaction: TransactionOptions,

#[arg(long)]
#[arg(help = "Generate Typescript bindings.")]
pub typescript: bool,

#[arg(long)]
#[arg(help = "Generate Typescript bindings.")]
pub typescript_v2: bool,

#[arg(long)]
#[arg(help = "Generate Unity bindings.")]
pub unity: bool,

#[arg(long)]
#[arg(help = "Output directory.", default_value = "bindings")]
pub bindings_output: String,

/// Specify the features to activate.
#[command(flatten)]
pub features: FeaturesSpec,

/// Specify packages to build.
#[command(flatten)]
pub packages: Option<PackagesFilter>,
}

impl DevArgs {
/// Watches the `src` directory that is found at the same level of the `Scarb.toml` manifest
/// of the project into the provided [`Config`].
///
/// When a change is detected, it rebuilds the project and applies the migrations.
pub fn run(self, config: &Config) -> Result<()> {
let (tx, rx) = channel();
let (file_tx, file_rx) = channel();
let (rebuild_tx, rebuild_rx) = channel();

let watcher_config = notify::Config::default().with_poll_interval(Duration::from_secs(1));
let watcher_config =
notify::Config::default().with_poll_interval(Duration::from_millis(500));

let mut watcher = PollWatcher::new(tx, watcher_config)?;
let mut watcher = PollWatcher::new(file_tx, watcher_config)?;

let watched_directory = config.manifest_path().parent().unwrap().join("src");

watcher.watch(watched_directory.as_std_path(), RecursiveMode::Recursive).unwrap();

// Always build the project before starting the dev loop to make sure that the project is
// in a valid state. Devs may not use `build` anymore when using `dev`.
BuildArgs::default().run(config)?;
// Initial build and migrate
let build_args = BuildArgs {
typescript: self.typescript,
typescript_v2: self.typescript_v2,
unity: self.unity,
bindings_output: self.bindings_output,
features: self.features,
packages: self.packages,
..Default::default()
};
build_args.clone().run(config)?;
info!("Initial build completed.");

let _ =
MigrateArgs::new_apply(self.world.clone(), self.starknet.clone(), self.account.clone())
.run(config);
let migrate_args = MigrateArgs {
world: self.world,
starknet: self.starknet,
account: self.account,
transaction: self.transaction,
};

let _ = migrate_args.clone().run(config);

info!(
directory = watched_directory.to_string(),
"Initial migration completed. Waiting for changes."
);

let mut e_handler = EventHandler;

loop {
let is_rebuild_needed = match rx.recv() {
Ok(maybe_event) => match maybe_event {
Ok(event) => e_handler.process_event(event),
let e_handler = EventHandler;
let rebuild_tx_clone = rebuild_tx.clone();

// Independent thread to handle file events and trigger rebuilds.
config.tokio_handle().spawn(async move {
loop {
match file_rx.recv() {
Ok(maybe_event) => match maybe_event {
Ok(event) => {
trace!(?event, "Event received.");

if e_handler.process_event(event) && rebuild_tx_clone.send(()).is_err()
{
break;
}
}
Err(error) => {
error!(?error, "Processing event.");
break;
}
},
Err(error) => {
error!(?error, "Processing event.");
error!(?error, "Receiving event.");
break;
}
},
Err(error) => {
error!(?error, "Receiving event.");
break;
}
};

if is_rebuild_needed {
// Ignore the fails of those commands as the `run` function
// already logs the error.
let _ = BuildArgs::default().run(config);

let _ = MigrateArgs::new_apply(
self.world.clone(),
self.starknet.clone(),
self.account.clone(),
)
.run(config);
}
});

// Main thread handles the rebuilds.
let mut last_event_time = None;
let debounce_period = Duration::from_millis(1500);

loop {
match rebuild_rx.try_recv() {
Ok(()) => {
last_event_time = Some(Instant::now());
}
Err(std::sync::mpsc::TryRecvError::Empty) => {
if let Some(last_time) = last_event_time {
if last_time.elapsed() >= debounce_period {
let _ = build_args.clone().run(config);
let _ = migrate_args.clone().run(config);
last_event_time = None;
} else {
trace!("Change detected, waiting for debounce period.");
}
}
thread::sleep(Duration::from_millis(300));
}
Err(std::sync::mpsc::TryRecvError::Disconnected) => break,
}
}

Ok(())
}
}

#[derive(Debug, Default)]
#[derive(Debug)]
struct EventHandler;

impl EventHandler {
/// Processes a debounced event and return true if a rebuild is needed.
/// Only considers Cairo file and the Scarb.toml manifest.
fn process_event(&mut self, event: Event) -> bool {
fn process_event(&self, event: Event) -> bool {
trace!(?event, "Processing event.");

let paths = match event.kind {
Expand Down
10 changes: 5 additions & 5 deletions bin/sozo/src/commands/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@ use super::options::transaction::TransactionOptions;
use super::options::world::WorldOptions;
use crate::utils;

#[derive(Debug, Args)]
#[derive(Debug, Clone, Args)]
pub struct MigrateArgs {
#[command(flatten)]
transaction: TransactionOptions,
pub transaction: TransactionOptions,

#[command(flatten)]
world: WorldOptions,
pub world: WorldOptions,

#[command(flatten)]
starknet: StarknetOptions,
pub starknet: StarknetOptions,

#[command(flatten)]
account: AccountOptions,
pub account: AccountOptions,
}

impl MigrateArgs {
Expand Down
Loading

0 comments on commit 61febd5

Please sign in to comment.