refactor: source builder (#655)

This commit is contained in:
Valerio Ageno
2025-03-26 18:20:29 +01:00
committed by GitHub
parent 6dbdafa888
commit 12aa12c94d
6 changed files with 206 additions and 146 deletions
+2 -2
View File
@@ -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<std::process::Output, std::io::Error> {
pub fn build_tuono_config(&mut self) -> io::Result<std::process::Output> {
if !Path::new(BUILD_TUONO_CONFIG).exists() {
eprintln!("Failed to find the build script. Please run `npm install`");
std::process::exit(1);
+8 -38
View File
@@ -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<App> {
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,
+7 -5
View File
@@ -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();
}
+14 -1
View File
@@ -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);
}
}
+160 -98
View File
@@ -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<Self> {
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, Route>) -> String {
@@ -141,67 +270,6 @@ fn create_modules_declaration(routes: &HashMap<String, Route>) -> String {
route_declarations
}
pub fn bundle_axum_source(mode: Mode) -> io::Result<App> {
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;"));
}
+15 -2
View File
@@ -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?",
));
}