mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
refactor: source builder (#655)
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;"));
|
||||
}
|
||||
|
||||
@@ -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?",
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user