diff --git a/crates/tuono/src/app.rs b/crates/tuono/src/app.rs index 14bf75fc..81dda92a 100644 --- a/crates/tuono/src/app.rs +++ b/crates/tuono/src/app.rs @@ -68,7 +68,7 @@ impl App { app } - fn collect_routes(&mut self) { + pub fn collect_routes(&mut self) { glob( self.base_path .join("src/routes/**/*.*") @@ -203,7 +203,7 @@ impl App { .expect("Failed to run the rust server") } - pub fn build_tuono_config(&mut self) -> Result { + pub fn build_tuono_config(&mut self) -> io::Result { if !Path::new(BUILD_TUONO_CONFIG).exists() { eprintln!("Failed to find the build script. Please run `npm install`"); std::process::exit(1); diff --git a/crates/tuono/src/cli.rs b/crates/tuono/src/cli.rs index 9fbd69b9..eabe3d3a 100644 --- a/crates/tuono/src/cli.rs +++ b/crates/tuono/src/cli.rs @@ -1,15 +1,9 @@ -use std::path::PathBuf; - use clap::{Parser, Subcommand}; use tracing::{span, Level}; -use tracing_subscriber::EnvFilter; -use crate::app::App; use crate::commands::{build, dev, new}; use crate::mode::Mode; -use crate::source_builder::{ - bundle_axum_source, check_tuono_folder, create_client_entry_files, generate_fallback_html, -}; +use crate::source_builder::SourceBuilder; #[derive(Subcommand, Debug)] enum Actions { @@ -46,29 +40,7 @@ struct Args { action: Actions, } -fn check_for_tuono_config() { - if !PathBuf::from("tuono.config.ts").exists() { - eprintln!("Cannot find tuono.config.ts - is this a tuono project?"); - std::process::exit(1); - } -} - -fn init_tuono_folder(mode: Mode) -> std::io::Result { - check_for_tuono_config(); - check_tuono_folder()?; - let app = bundle_axum_source(mode)?; - create_client_entry_files()?; - - Ok(app) -} - pub fn app() -> std::io::Result<()> { - tracing_subscriber::fmt() - // Not need for the time since the code is synchronous - .without_time() - .with_env_filter(EnvFilter::from_default_env()) - .init(); - let args = Args::parse(); match args.action { @@ -77,24 +49,22 @@ pub fn app() -> std::io::Result<()> { let _guard = span.enter(); - let mut app = init_tuono_folder(Mode::Dev)?; + let mut source_builder = SourceBuilder::new(Mode::Dev)?; - app.build_tuono_config() - .expect("Failed to build tuono.config.ts"); + source_builder.base_build()?; - generate_fallback_html(&app)?; - app.check_server_availability(Mode::Dev); + source_builder.app.check_server_availability(Mode::Dev); - dev::watch().unwrap(); + dev::watch(source_builder).unwrap(); } Actions::Build { ssg, no_js_emit } => { let span = span!(Level::TRACE, "BUILD"); let _guard = span.enter(); - let app = init_tuono_folder(Mode::Prod)?; - - build::build(app, ssg, no_js_emit); + let mut source_builder = SourceBuilder::new(Mode::Prod)?; + source_builder.base_build()?; + build::build(source_builder.app, ssg, no_js_emit); } Actions::New { folder_name, diff --git a/crates/tuono/src/commands/dev.rs b/crates/tuono/src/commands/dev.rs index 21c2c3a7..504307b3 100644 --- a/crates/tuono/src/commands/dev.rs +++ b/crates/tuono/src/commands/dev.rs @@ -1,6 +1,6 @@ use std::fs; use std::path::Path; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use watchexec_supervisor::command::{Command, Program}; use miette::{IntoDiagnostic, Result}; @@ -8,8 +8,7 @@ use watchexec::Watchexec; use watchexec_signals::Signal; use watchexec_supervisor::job::{start_job, Job}; -use crate::mode::Mode; -use crate::source_builder::bundle_axum_source; +use crate::source_builder::SourceBuilder; use console::Term; use spinners::{Spinner, Spinners}; @@ -87,7 +86,8 @@ fn ssr_reload_needed(path: &Path) -> bool { } #[tokio::main] -pub async fn watch() -> Result<()> { +pub async fn watch(source_builder: SourceBuilder) -> Result<()> { + let source_builder = RwLock::new(source_builder); let term = Term::stdout(); let mut sp = Spinner::new(Spinners::Dots, "Starting dev server...".into()); @@ -138,7 +138,9 @@ pub async fn watch() -> Result<()> { if should_reload_rust_server { println!(" Reloading..."); rust_server.stop(); - bundle_axum_source(Mode::Dev).expect("Failed to bundle rust source"); + let mut builder = source_builder.write().unwrap(); + builder.app.collect_routes(); + _ = builder.refresh_axum_source(); rust_server.start(); } diff --git a/crates/tuono/src/main.rs b/crates/tuono/src/main.rs index a145c832..3da5290e 100644 --- a/crates/tuono/src/main.rs +++ b/crates/tuono/src/main.rs @@ -1,5 +1,18 @@ +use tracing::error; +use tracing_subscriber::EnvFilter; use tuono::cli::app; fn main() { - app().expect("Failed to start the CLI") + tracing_subscriber::fmt() + // Time not needed since the execution is synchronous + .without_time() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + if let Err(e) = app() { + // Generic error. + // Recoverable errors should be managed locally + error!("Failed to run the tuono CLI: {}", e); + std::process::exit(1); + } } diff --git a/crates/tuono/src/source_builder.rs b/crates/tuono/src/source_builder.rs index 9ac324b5..6edda842 100644 --- a/crates/tuono/src/source_builder.rs +++ b/crates/tuono/src/source_builder.rs @@ -3,6 +3,7 @@ use std::fs; use std::io; use std::io::prelude::*; use std::path::Path; +use std::path::PathBuf; use clap::crate_version; use tracing::error; @@ -77,16 +78,144 @@ async fn main() { } "##; +#[cfg(target_os = "windows")] +const MAIN_FILE_PATH: &str = ".\\.tuono\\main.rs"; + +#[cfg(target_os = "windows")] +const FALLBACK_HTML_PATH: &str = ".\\.tuono\\index.html"; + +#[cfg(not(target_os = "windows"))] +const MAIN_FILE_PATH: &str = "./.tuono/main.rs"; + +#[cfg(not(target_os = "windows"))] +const FALLBACK_HTML_PATH: &str = "./.tuono/index.html"; + const ROUTE_FOLDER: &str = "src/routes"; const DEV_FOLDER: &str = ".tuono"; -fn create_main_file(base_path: &Path, bundled_file: &String) { - let mut data_file = - fs::File::create(base_path.join(".tuono/main.rs")).expect("creation failed"); +// Use this function to instruct the users on how to +// fix their setup to make tuono work +fn recoverable_error(message: &str) -> ! { + error!("{}", message); + std::process::exit(1); +} - data_file - .write_all(bundled_file.as_bytes()) - .expect("write failed"); +// Struct to build the source code +// on both "dev" and "build" commands +#[derive(Clone, Debug)] +pub struct SourceBuilder { + pub app: App, + mode: Mode, + base_path: PathBuf, +} + +impl SourceBuilder { + pub fn new(mode: Mode) -> io::Result { + if !PathBuf::from("tuono.config.ts").exists() { + recoverable_error("Cannot find tuono.config.ts - is this a tuono project?"); + } + + let dev_folder = Path::new(DEV_FOLDER); + if !&dev_folder.is_dir() { + fs::create_dir(dev_folder)?; + } + + let app = App::new(); + + let base_path = std::env::current_dir()?; + + create_client_entry_files()?; + + Ok(Self { + app, + mode, + base_path, + }) + } + + // Build the source code needed for both build and dev + pub fn base_build(&mut self) -> io::Result<()> { + let Self { mode, .. } = &self; + + self.refresh_axum_source()?; + + if mode == &Mode::Dev { + self.app.build_tuono_config()?; + let fallback_html = create_html_fallback(&self.app); + self.create_file(FALLBACK_HTML_PATH, &fallback_html)?; + } + + Ok(()) + } + + fn generate_axum_source(&self) -> String { + let Self { app, mode, .. } = &self; + + let src = AXUM_ENTRY_POINT + .replace( + "// ROUTE_BUILDER\n", + &create_routes_declaration(&app.route_map), + ) + .replace( + "// MODULE_IMPORTS\n", + &create_modules_declaration(&app.route_map), + ) + .replace("/*VERSION*/", crate_version!()) + .replace("/*MODE*/", mode.as_str()) + .replace( + "//MAIN_FILE_IMPORT//", + if app.has_app_state { + r#"#[path="../src/app.rs"] + mod tuono_main_state; + "# + } else { + "" + }, + ) + .replace( + "//MAIN_FILE_DEFINITION//", + if app.has_app_state { + "let user_custom_state = tuono_main_state::main();" + } else { + "" + }, + ) + .replace( + "//MAIN_FILE_USAGE//", + if app.has_app_state { + ".with_state(user_custom_state)" + } else { + "" + }, + ); + + let mut import_http_handler = String::new(); + + let used_http_methods = app.get_used_http_methods(); + + for method in used_http_methods.into_iter() { + let method = method.to_string().to_lowercase(); + import_http_handler.push_str(&format!("use tuono_lib::axum::routing::{method};\n")) + } + + src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler) + } + + pub fn refresh_axum_source(&self) -> io::Result<()> { + let axum_source = self.generate_axum_source(); + + self.create_file(MAIN_FILE_PATH, &axum_source)?; + + Ok(()) + } + + fn create_file(&self, path: &str, content: &str) -> io::Result<()> { + let mut data_file = fs::File::create(self.base_path.join(path))?; + + data_file.write_all(content.as_bytes())?; + + Ok(()) + } } fn create_routes_declaration(routes: &HashMap) -> String { @@ -141,67 +270,6 @@ fn create_modules_declaration(routes: &HashMap) -> String { route_declarations } -pub fn bundle_axum_source(mode: Mode) -> io::Result { - let base_path = std::env::current_dir()?; - - let app = App::new(); - let bundled_file = generate_axum_source(&app, mode); - create_main_file(&base_path, &bundled_file); - - Ok(app) -} - -fn generate_axum_source(app: &App, mode: Mode) -> String { - let src = AXUM_ENTRY_POINT - .replace( - "// ROUTE_BUILDER\n", - &create_routes_declaration(&app.route_map), - ) - .replace( - "// MODULE_IMPORTS\n", - &create_modules_declaration(&app.route_map), - ) - .replace("/*VERSION*/", crate_version!()) - .replace("/*MODE*/", mode.as_str()) - .replace( - "//MAIN_FILE_IMPORT//", - if app.has_app_state { - r#"#[path="../src/app.rs"] - mod tuono_main_state; - "# - } else { - "" - }, - ) - .replace( - "//MAIN_FILE_DEFINITION//", - if app.has_app_state { - "let user_custom_state = tuono_main_state::main();" - } else { - "" - }, - ) - .replace( - "//MAIN_FILE_USAGE//", - if app.has_app_state { - ".with_state(user_custom_state)" - } else { - "" - }, - ); - - let mut import_http_handler = String::new(); - - let used_http_methods = app.get_used_http_methods(); - - for method in used_http_methods.into_iter() { - let method = method.to_string().to_lowercase(); - import_http_handler.push_str(&format!("use tuono_lib::axum::routing::{method};\n")) - } - - src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler) -} - fn create_html_fallback(app: &App) -> String { if let Some(config) = app.config.as_ref() { if let Some(origin) = &config.server.origin { @@ -215,29 +283,6 @@ fn create_html_fallback(app: &App) -> String { } } -pub fn generate_fallback_html(app: &App) -> io::Result<()> { - let base_path = std::env::current_dir().unwrap_or_else(|_| { - error!("Failed to get current directory"); - std::process::exit(1); - }); - let mut data_file = fs::File::create(base_path.join(".tuono/index.html"))?; - - let fallback_html = create_html_fallback(app); - - data_file.write_all(fallback_html.as_bytes())?; - - Ok(()) -} - -pub fn check_tuono_folder() -> io::Result<()> { - let dev_folder = Path::new(DEV_FOLDER); - if !&dev_folder.is_dir() { - fs::create_dir(dev_folder)?; - } - - Ok(()) -} - pub fn create_client_entry_files() -> io::Result<()> { let dev_folder = Path::new(DEV_FOLDER); @@ -257,10 +302,19 @@ mod tests { #[test] fn should_set_the_correct_mode() { - let source_builder = App::new(); + let dev_bundle = SourceBuilder { + app: App::new(), + mode: Mode::Dev, + base_path: PathBuf::new(), + } + .generate_axum_source(); - let dev_bundle = generate_axum_source(&source_builder, Mode::Dev); - let prod_bundle = generate_axum_source(&source_builder, Mode::Prod); + let prod_bundle = SourceBuilder { + app: App::new(), + mode: Mode::Prod, + base_path: PathBuf::new(), + } + .generate_axum_source(); assert!(dev_bundle.contains("const MODE: Mode = Mode::Dev;")); assert!(prod_bundle.contains("const MODE: Mode = Mode::Prod;")); @@ -268,25 +322,33 @@ mod tests { #[test] fn should_not_load_the_axum_get_function() { - let source_builder = App::new(); - - let dev_bundle = generate_axum_source(&source_builder, Mode::Dev); + let dev_bundle = SourceBuilder { + app: App::new(), + mode: Mode::Dev, + base_path: PathBuf::new(), + } + .generate_axum_source(); assert!(!dev_bundle.contains("use tuono_lib::axum::routing::get;")); } #[test] fn should_load_the_axum_get_function() { - let mut source_builder = App::new(); + let mut source_builder = SourceBuilder { + app: App::new(), + mode: Mode::Dev, + base_path: PathBuf::new(), + }; let mut route = Route::new(String::from("index.tsx")); route.update_axum_info(); source_builder + .app .route_map .insert(String::from("index.rs"), route); - let dev_bundle = generate_axum_source(&source_builder, Mode::Dev); + let dev_bundle = source_builder.generate_axum_source(); assert!(dev_bundle.contains("use tuono_lib::axum::routing::get;")); } diff --git a/crates/tuono/tests/cli_build.rs b/crates/tuono/tests/cli_build.rs index 52c6a5e4..b7cdd42a 100644 --- a/crates/tuono/tests/cli_build.rs +++ b/crates/tuono/tests/cli_build.rs @@ -1,6 +1,7 @@ use assert_cmd::Command; use serial_test::serial; use std::fs; +use tracing::Level; mod utils; use utils::temp_tuono_project::TempTuonoProject; @@ -8,6 +9,10 @@ use utils::temp_tuono_project::TempTuonoProject; const POST_API_FILE: &str = r"#[tuono_lib::api(POST)]"; const GET_API_FILE: &str = r"#[tuono_lib::api(GET)]"; +fn tracing_message(level: Level, module: &str, message: &str) -> String { + format!("\x1b[31m{level}\x1b[0m \x1b[2mtuono::{module}\x1b[0m\x1b[2m:\x1b[0m {message}\n") +} + #[cfg(target_os = "windows")] const BUILD_TUONO_CONFIG: &str = ".\\node_modules\\.bin\\tuono-build-config.cmd"; @@ -185,7 +190,11 @@ fn dev_fails_with_no_config() { .arg("dev") .assert() .failure() - .stderr("Cannot find tuono.config.ts - is this a tuono project?\n"); + .stdout(tracing_message( + Level::ERROR, + "source_builder", + "Cannot find tuono.config.ts - is this a tuono project?", + )); } #[test] @@ -198,5 +207,9 @@ fn build_fails_with_no_config() { .arg("dev") .assert() .failure() - .stderr("Cannot find tuono.config.ts - is this a tuono project?\n"); + .stdout(tracing_message( + Level::ERROR, + "source_builder", + "Cannot find tuono.config.ts - is this a tuono project?", + )); }