Merge remote-tracking branch 'origin/main' into remove-windows-vite-log-warning

This commit is contained in:
Valerio Ageno
2025-04-09 09:51:07 +02:00
44 changed files with 1674 additions and 1147 deletions
+5 -13
View File
@@ -45,17 +45,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 1
if: always()
needs: [e2e]
env:
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
needs:
- e2e
steps:
- name: Check for failure or cancelled jobs result
shell: bash
run: |
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
exit 0
else
exit 1
fi
- name: Exit with error if some jobs are not successful
run: exit 1
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) }}
+5 -13
View File
@@ -35,17 +35,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 1
if: always()
needs: [format]
env:
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
needs:
- format
steps:
- name: Check for failure or cancelled jobs result
shell: bash
run: |
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
exit 0
else
exit 1
fi
- name: Exit with error if some jobs are not successful
run: exit 1
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) }}
+6 -13
View File
@@ -70,17 +70,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 1
if: always()
needs: [build_and_test, lint_and_fmt]
env:
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
needs:
- build_and_test
- lint_and_fmt
steps:
- name: Check for failure or cancelled jobs result
shell: bash
run: |
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
exit 0
else
exit 1
fi
- name: Exit with error if some jobs are not successful
run: exit 1
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) }}
+6 -13
View File
@@ -64,17 +64,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 1
if: always()
needs: [build_and_test, lint_and_fmt]
env:
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
needs:
- build_and_test
- lint_and_fmt
steps:
- name: Check for failure or cancelled jobs result
shell: bash
run: |
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
exit 0
else
exit 1
fi
- name: Exit with error if some jobs are not successful
run: exit 1
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) }}
+1 -8
View File
@@ -1,9 +1,2 @@
* @Valerioageno @marcalexiei
# Rust
/crates/ @Valerioageno
/Cargo.toml @Valerioageno
# Misc
/.github/ @marcalexiei
* @Valerioageno
+8 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono"
version = "0.19.0"
version = "0.19.2"
edition = "2024"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "Superfast React fullstack framework"
@@ -10,7 +10,7 @@ repository = "https://github.com/tuono-labs/tuono"
readme = "../../README.md"
license-file = "../../LICENSE.md"
categories = ["web-programming"]
include = ["src/**/*.rs", "Cargo.toml"]
include = ["src/**/*.rs", "templates/*", "Cargo.toml"]
[lib]
name = "tuono"
@@ -18,21 +18,24 @@ path = "src/lib.rs"
[dependencies]
clap = { version = "4.5.4", features = ["derive", "cargo"] }
watchexec = "5.0.0"
tracing = "0.1.41"
tracing-subscriber = {version = "0.3.19", features = ["env-filter"]}
miette = "7.2.0"
colored = "2.1.0"
watchexec = "5.0.0"
watchexec-signals = "4.0.0"
watchexec-events = "4.0.0"
watchexec-supervisor = "3.0.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0.202", features = ["derive"] }
watchexec-supervisor = "3.0.0"
glob = "0.3.1"
regex = "1.10.4"
reqwest = { version = "0.12.4", features = ["blocking", "json"] }
serde_json = "1.0"
fs_extra = "1.3.0"
http = "1.1.0"
tuono_internal = {path = "../tuono_internal", version = "0.19.0"}
tuono_internal = {path = "../tuono_internal", version = "0.19.2"}
spinners = "4.1.1"
console = "0.15.10"
+98 -107
View File
@@ -1,79 +1,22 @@
use std::fs;
use std::path::Path;
use std::sync::{Arc, RwLock};
use watchexec_supervisor::command::{Command, Program};
use std::sync::RwLock;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tuono_internal::tuono_println;
use watchexec_events::Tag;
use watchexec_events::filekind::FileEventKind;
use crate::process_manager::{ProcessId, ProcessManager};
use miette::{IntoDiagnostic, Result};
use watchexec::Watchexec;
use watchexec_signals::Signal;
use watchexec_supervisor::job::{Job, start_job};
use crate::source_builder::SourceBuilder;
use console::Term;
use spinners::{Spinner, Spinners};
#[cfg(target_os = "windows")]
const DEV_WATCH_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-watch.cmd";
#[cfg(target_os = "windows")]
const DEV_SSR_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-ssr.cmd";
#[cfg(not(target_os = "windows"))]
const DEV_WATCH_BIN_SRC: &str = "node_modules/.bin/tuono-dev-watch";
#[cfg(not(target_os = "windows"))]
const DEV_SSR_BIN_SRC: &str = "node_modules/.bin/tuono-dev-ssr";
fn watch_react_src() -> Job {
if !Path::new(DEV_SSR_BIN_SRC).exists() {
eprintln!("Failed to find script to run dev watch. Please run `npm install`");
std::process::exit(1);
}
start_job(Arc::new(Command {
program: Program::Exec {
prog: DEV_WATCH_BIN_SRC.into(),
args: vec![],
},
options: Default::default(),
}))
.0
}
fn run_rust_dev_server() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "cargo".into(),
args: vec!["run".to_string(), "-q".to_string()],
},
options: Default::default(),
}))
.0
}
fn build_rust_src() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "cargo".into(),
args: vec!["build".to_string(), "-q".to_string()],
},
options: Default::default(),
}))
.0
}
fn build_react_ssr_src() -> Job {
if !Path::new(DEV_SSR_BIN_SRC).exists() {
eprintln!("Failed to find script to run dev ssr. Please run `npm install`");
std::process::exit(1);
}
start_job(Arc::new(Command {
program: Program::Exec {
prog: DEV_SSR_BIN_SRC.into(),
args: vec![],
},
options: Default::default(),
}))
.0
}
fn ssr_reload_needed(path: &Path) -> bool {
let file_name_starts_with_env = path
.file_name()
@@ -85,73 +28,121 @@ fn ssr_reload_needed(path: &Path) -> bool {
file_name_starts_with_env || file_path.ends_with("sx") || file_path.ends_with("mdx")
}
#[allow(
clippy::await_holding_lock,
reason = "At this point there is no other thread waiting for the lock"
)]
async fn start_all_processes(process_manager: Arc<Mutex<ProcessManager>>) {
if let Ok(mut pm) = process_manager.lock() {
pm.start_dev_processes().await
}
}
fn detect_existing_env_files() -> Vec<String> {
if let Ok(dir) = fs::read_dir("./") {
dir.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_name().to_string_lossy().starts_with(".env"))
.map(|entry| entry.path().to_string_lossy().into_owned())
.collect::<Vec<String>>()
} else {
Vec::new()
}
}
#[tokio::main]
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());
watch_react_src().start().await;
let process_manager = Arc::new(Mutex::new(ProcessManager::new()));
let rust_server = run_rust_dev_server();
let build_rust_src = build_rust_src();
let env_files = detect_existing_env_files();
let build_ssr_bundle = build_react_ssr_src();
let env_files = fs::read_dir("./")
.expect("Error reading env files from current directory")
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_name().to_string_lossy().starts_with(".env"))
.map(|entry| entry.path().to_string_lossy().into_owned())
.collect::<Vec<String>>();
build_ssr_bundle.start().await;
build_rust_src.start().await;
// Wait vite and rust builds
build_ssr_bundle.to_wait().await;
build_rust_src.to_wait().await;
rust_server.start().await;
start_all_processes(process_manager.clone()).await;
// Remove the spinner
sp.stop();
term.clear_line().unwrap();
_ = term.clear_line();
// Server address log for production
// is done on the server process.
if let Ok(builder) = source_builder.read() {
if let Ok(pm) = process_manager.lock() {
pm.log_server_address(builder.app.config.clone().unwrap_or_default());
}
}
let wx = Watchexec::new(move |mut action| {
let process_manager = process_manager.clone();
// if Ctrl-C is received, quit
if action.signals().any(|sig| sig == Signal::Interrupt) {
if let Ok(mut pm) = process_manager.lock() {
pm.abort_all();
}
action.quit_gracefully(Signal::Quit, Duration::from_secs(30));
return action;
}
// The best way to trigger the processes is to use these
// flags.
// This because the same event can be triggered multiple times
// and we don't want to restart the process multiple times for the same
// purpose.
let mut should_reload_ssr_bundle = false;
let mut should_reload_rust_server = false;
let mut should_refresh_axum_source = false;
for event in action.events.iter() {
for path in event.paths() {
let file_path = path.0.to_string_lossy();
if file_path.ends_with(".rs") {
should_reload_rust_server = true
}
for event_type in event.tags.iter() {
if let Tag::FileEventKind(kind) = event_type {
match kind {
FileEventKind::Remove(_) => {
if event.paths().any(|(path, _)| {
path.extension().is_some_and(|ext| ext == "rs") ||
// APIs might define new HTTP methods that requires
// a refresh of the axum source
path.to_str().unwrap_or("").contains("api")
}) {
should_refresh_axum_source = true;
}
}
FileEventKind::Modify(_) => {
if event
.paths()
.any(|(path, _)| path.extension().is_some_and(|ext| ext == "rs"))
{
should_reload_rust_server = true;
}
if ssr_reload_needed(path.0) {
should_reload_ssr_bundle = true
if event.paths().any(|(path, _)| ssr_reload_needed(path)) {
should_reload_ssr_bundle = true;
}
}
_ => {}
}
}
}
}
if should_reload_rust_server {
println!(" Reloading...");
rust_server.stop();
let mut builder = source_builder.write().unwrap();
builder.app.collect_routes();
_ = builder.refresh_axum_source();
rust_server.start();
if should_reload_rust_server || should_refresh_axum_source {
tuono_println!("Reloading...");
if should_refresh_axum_source {
if let Ok(mut builder) = source_builder.write() {
builder.app.collect_routes();
_ = builder.refresh_axum_source();
}
}
if let Ok(mut pm) = process_manager.lock() {
pm.restart_process(ProcessId::RunRustDevServer);
}
}
if should_reload_ssr_bundle {
build_ssr_bundle.stop();
build_ssr_bundle.start();
}
// if Ctrl-C is received, quit
if action.signals().any(|sig| sig == Signal::Interrupt) {
action.quit();
if let Ok(mut pm) = process_manager.lock() {
pm.restart_process(ProcessId::BuildReactSSRSrc);
}
}
action
+15
View File
@@ -6,6 +6,8 @@ use std::env;
use std::fs::{self, File, OpenOptions, create_dir};
use std::io::{self, prelude::*};
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::trace;
#[derive(Deserialize, Debug)]
enum GithubFileType {
@@ -156,6 +158,9 @@ pub fn create_new_project(
update_package_json_version(&folder_path).expect("Failed to update package.json version");
update_cargo_toml_version(&folder_path).expect("Failed to update Cargo.toml version");
init_new_git_repo(&folder_path);
outro(folder);
}
@@ -265,6 +270,16 @@ fn update_cargo_toml_version(folder_path: &Path) -> io::Result<()> {
Ok(())
}
fn init_new_git_repo(folder_path: &Path) {
if let Ok(output) = Command::new("git").arg("init").arg(folder_path).output() {
if !output.status.success() {
trace!("Failed to initialise a new git repo")
}
} else {
trace!("Failed to initialise a new git repo")
}
}
fn outro(folder_name: String) {
println!("Success! 🎉");
+1
View File
@@ -7,5 +7,6 @@ mod app;
pub mod cli;
mod commands;
mod mode;
mod process_manager;
mod route;
mod source_builder;
+140
View File
@@ -0,0 +1,140 @@
use std::collections::HashMap;
use clap::crate_version;
use std::path::Path;
use std::sync::Arc;
use tokio::task::JoinHandle;
use colored::Colorize;
use tracing::trace;
use tuono_internal::config::Config;
use tuono_internal::tuono_println;
use watchexec_supervisor::command::{Command, Program};
use watchexec_supervisor::job::{Job, start_job};
#[cfg(target_os = "windows")]
const DEV_WATCH_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-watch.cmd";
#[cfg(target_os = "windows")]
const DEV_SSR_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-ssr.cmd";
#[cfg(not(target_os = "windows"))]
const DEV_WATCH_BIN_SRC: &str = "node_modules/.bin/tuono-dev-watch";
#[cfg(not(target_os = "windows"))]
const DEV_SSR_BIN_SRC: &str = "node_modules/.bin/tuono-dev-ssr";
#[derive(PartialEq, Eq, Hash, Debug)]
pub enum ProcessId {
WatchReactSrc,
RunRustDevServer,
BuildRustSrc,
BuildReactSSRSrc,
}
// This struct manages all the processes spawned by the dev server
// That are not the dev server itself
#[derive(Debug)]
pub struct ProcessManager {
processes: HashMap<ProcessId, (Job, JoinHandle<()>)>,
}
impl ProcessManager {
pub fn new() -> Self {
trace!("Creating process manager");
if !Path::new(DEV_SSR_BIN_SRC).exists() {
eprintln!("Failed to find script to run dev watch. Please run `npm install`");
std::process::exit(1);
}
let mut processes = HashMap::new();
processes.insert(
ProcessId::WatchReactSrc,
start_supervisor_job(DEV_WATCH_BIN_SRC, vec![]),
);
processes.insert(
ProcessId::RunRustDevServer,
start_supervisor_job("cargo", vec!["run", "-q"]),
);
processes.insert(
ProcessId::BuildRustSrc,
start_supervisor_job("cargo", vec!["build", "-q"]),
);
processes.insert(
ProcessId::BuildReactSSRSrc,
start_supervisor_job(DEV_SSR_BIN_SRC, vec![]),
);
Self { processes }
}
pub fn start_process(&mut self, id: ProcessId) {
trace!("start process {:?}", id);
if let Some((job, _)) = self.processes.get(&id) {
job.start();
}
}
// Start all the processes needed for the dev server
pub async fn start_dev_processes(&mut self) {
trace!("Starting dev processes");
self.start_process(ProcessId::WatchReactSrc);
self.start_process(ProcessId::BuildRustSrc);
self.start_process(ProcessId::BuildReactSSRSrc);
self.wait_for_process(ProcessId::BuildRustSrc).await;
self.wait_for_process(ProcessId::BuildReactSSRSrc).await;
// This needs to start after having built the rust and react sources
self.start_process(ProcessId::RunRustDevServer);
}
pub fn log_server_address(&self, config: Config) {
let server_address = format!("{}:{}", config.server.host, config.server.port);
// Format the server address as a valid URL so that it becomes clickable in the CLI
// @see https://github.com/tuono-labs/tuono/issues/460
let server_base_url = format!("http://{}", server_address);
println!();
tuono_println!("⚡ Tuono v{}", crate_version!());
tuono_println!("Development server at: {}\n", server_base_url.blue().bold());
if let Some(origin) = config.server.origin {
tuono_println!("Origin: {}\n", origin.blue().bold());
}
}
pub fn restart_process(&mut self, id: ProcessId) {
trace!("Restarting process {:?}", id);
if let Some((job, _)) = self.processes.get(&id) {
job.stop();
job.start();
}
}
pub async fn wait_for_process(&mut self, id: ProcessId) {
trace!("Waiting for process {:?}", id);
if let Some((job, _)) = self.processes.get(&id) {
job.to_wait().await;
}
}
pub fn abort_all(&mut self) {
trace!("Aborting all processes");
for (_, (job, handle)) in self.processes.iter() {
job.stop();
handle.abort();
}
}
}
fn start_supervisor_job(prog: &str, args: Vec<&str>) -> (Job, JoinHandle<()>) {
start_job(Arc::new(Command {
program: Program::Exec {
prog: prog.into(),
args: args.into_iter().map(|arg| arg.to_string()).collect(),
},
options: Default::default(),
}))
}
+86 -142
View File
@@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::fs;
use std::io;
use std::io::prelude::*;
@@ -13,76 +12,14 @@ use crate::mode::Mode;
use crate::route::AxumInfo;
use crate::route::Route;
const FALLBACK_HTML: &str = r#"<!doctype html>
<html>
<body>
<script>
window['__TUONO_SERVER_PAYLOAD__'] = [SERVER_PAYLOAD]
</script>
<script type="module" async>
import RefreshRuntime from '[BASE_URL]/vite-server/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => { }
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" async src="[BASE_URL]/vite-server/@vite/client"></script>
<script type="module" async src="[BASE_URL]/vite-server/client-main.tsx"></script>
</body>
</html>"#;
const SERVER_ENTRY_DATA: &str = "// File automatically generated by tuono
// Do not manually update this file
import { routeTree } from './routeTree.gen'
import { serverSideRendering } from 'tuono/ssr'
export const renderFn = serverSideRendering(routeTree)
";
const CLIENT_ENTRY_DATA: &str = "// File automatically generated by tuono
// Do not manually update this file
import 'vite/modulepreload-polyfill'
import { hydrate } from 'tuono/hydration'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
hydrate(routeTree)
";
const AXUM_ENTRY_POINT: &str = r##"
// File automatically generated
// Do not manually change it
use tuono_lib::{tokio, Mode, Server, axum::Router, tuono_internal_init_v8_platform};
// AXUM_GET_ROUTE_HANDLER
const MODE: Mode = /*MODE*/;
// MODULE_IMPORTS
//MAIN_FILE_IMPORT//
#[tokio::main]
async fn main() {
tuono_internal_init_v8_platform();
println!("\n ⚡ Tuono v/*VERSION*/");
//MAIN_FILE_DEFINITION//
let router = Router::new()
// ROUTE_BUILDER
//MAIN_FILE_USAGE//;
Server::init(router, MODE).await.start().await
}
"##;
#[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 FALLBACK_HTML: &str = include_str!("../templates/fallback.html");
#[cfg(not(target_os = "windows"))]
const SERVER_ENTRY_DATA: &str = include_str!("../templates/server.ts");
#[cfg(not(target_os = "windows"))]
const CLIENT_ENTRY_DATA: &str = include_str!("../templates/client.ts");
#[cfg(not(target_os = "windows"))]
const AXUM_ENTRY_POINT: &str = include_str!("../templates/server.rs");
#[cfg(not(target_os = "windows"))]
const MAIN_FILE_PATH: &str = "./.tuono/main.rs";
@@ -93,6 +30,21 @@ const FALLBACK_HTML_PATH: &str = "./.tuono/index.html";
const ROUTE_FOLDER: &str = "src/routes";
const DEV_FOLDER: &str = ".tuono";
#[cfg(target_os = "windows")]
const FALLBACK_HTML: &str = include_str!("..\\templates\\fallback.html");
#[cfg(target_os = "windows")]
const SERVER_ENTRY_DATA: &str = include_str!("..\\templates\\server.ts");
#[cfg(target_os = "windows")]
const CLIENT_ENTRY_DATA: &str = include_str!("..\\templates\\client.ts");
#[cfg(target_os = "windows")]
const AXUM_ENTRY_POINT: &str = include_str!("..\\templates\\server.rs");
#[cfg(target_os = "windows")]
const MAIN_FILE_PATH: &str = ".\\.tuono\\main.rs";
#[cfg(target_os = "windows")]
const FALLBACK_HTML_PATH: &str = ".\\.tuono\\index.html";
// Use this function to instruct the users on how to
// fix their setup to make tuono work
fn recoverable_error(message: &str) -> ! {
@@ -124,8 +76,6 @@ impl SourceBuilder {
let base_path = std::env::current_dir()?;
create_client_entry_files()?;
Ok(Self {
app,
mode,
@@ -138,11 +88,14 @@ impl SourceBuilder {
let Self { mode, .. } = &self;
self.refresh_axum_source()?;
let dev_folder = Path::new(DEV_FOLDER);
self.create_file(dev_folder.join("server-main.tsx"), SERVER_ENTRY_DATA)?;
self.create_file(dev_folder.join("client-main.tsx"), CLIENT_ENTRY_DATA)?;
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)?;
let fallback_html = self.build_html_fallback();
self.create_file(PathBuf::from(FALLBACK_HTML_PATH), &fallback_html)?;
}
Ok(())
@@ -152,14 +105,9 @@ impl SourceBuilder {
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("\r", "")
.replace("// ROUTE_BUILDER\n", &self.create_routes_declaration())
.replace("// MODULE_IMPORTS\n", &self.create_modules_declaration())
.replace("/*VERSION*/", crate_version!())
.replace("/*MODE*/", mode.as_str())
.replace(
@@ -204,97 +152,87 @@ impl SourceBuilder {
pub fn refresh_axum_source(&self) -> io::Result<()> {
let axum_source = self.generate_axum_source();
self.create_file(MAIN_FILE_PATH, &axum_source)?;
self.create_file(PathBuf::from(MAIN_FILE_PATH), &axum_source)?;
Ok(())
}
fn create_file(&self, path: &str, content: &str) -> io::Result<()> {
fn create_file(&self, path: PathBuf, 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 {
let mut route_declarations = String::from("// ROUTE_BUILDER\n");
fn create_routes_declaration(&self) -> String {
let routes = &self.app.route_map;
let mut route_declarations = String::from("// ROUTE_BUILDER\n");
for (_, route) in routes.iter() {
let Route { axum_info, .. } = &route;
for (_, route) in routes.iter() {
let Route { axum_info, .. } = &route;
if axum_info.is_some() {
let AxumInfo {
axum_route,
module_import,
} = axum_info.as_ref().unwrap();
if axum_info.is_some() {
let AxumInfo {
axum_route,
module_import,
} = axum_info.as_ref().unwrap();
if !route.is_api() {
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::tuono_internal_route))"#
));
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}", get({module_import}::tuono_internal_api))"#
));
} else {
for method in route.api_data.as_ref().unwrap().methods.clone() {
let method = method.to_string().to_lowercase();
if !route.is_api() {
route_declarations.push_str(&format!(
r#".route("{axum_route}", {method}({module_import}::{method}_tuono_internal_api))"#
r#".route("{axum_route}", get({module_import}::tuono_internal_route))"#
));
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}", get({module_import}::tuono_internal_api))"#
));
} else {
for method in route.api_data.as_ref().unwrap().methods.clone() {
let method = method.to_string().to_lowercase();
route_declarations.push_str(&format!(
r#".route("{axum_route}", {method}({module_import}::{method}_tuono_internal_api))"#
));
}
}
}
}
route_declarations
}
route_declarations
}
fn create_modules_declaration(&self) -> String {
let routes = &self.app.route_map;
let mut route_declarations = String::from("// MODULE_IMPORTS\n");
fn create_modules_declaration(routes: &HashMap<String, Route>) -> String {
let mut route_declarations = String::from("// MODULE_IMPORTS\n");
for (path, route) in routes.iter() {
if route.axum_info.is_some() {
let AxumInfo { module_import, .. } = route.axum_info.as_ref().unwrap();
for (path, route) in routes.iter() {
if route.axum_info.is_some() {
let AxumInfo { module_import, .. } = route.axum_info.as_ref().unwrap();
route_declarations.push_str(&format!(
r#"#[path="../{ROUTE_FOLDER}{path}.rs"]
route_declarations.push_str(&format!(
r#"#[path="../{ROUTE_FOLDER}{path}.rs"]
mod {module_import};
"#
))
))
}
}
route_declarations
}
route_declarations
}
fn create_html_fallback(app: &App) -> String {
if let Some(config) = app.config.as_ref() {
if let Some(origin) = &config.server.origin {
FALLBACK_HTML.replace("[BASE_URL]", origin)
fn build_html_fallback(&self) -> String {
if let Some(config) = &self.app.config.as_ref() {
if let Some(origin) = &config.server.origin {
FALLBACK_HTML.replace("[BASE_URL]", origin)
} else {
let url = format!("http://{}:{}", config.server.host, config.server.port);
FALLBACK_HTML.replace("[BASE_URL]", url.as_str())
}
} else {
let url = format!("http://{}:{}", config.server.host, config.server.port);
FALLBACK_HTML.replace("[BASE_URL]", url.as_str())
"".to_string()
}
} else {
"".to_string()
}
}
pub fn create_client_entry_files() -> io::Result<()> {
let dev_folder = Path::new(DEV_FOLDER);
let mut server_entry = fs::File::create(dev_folder.join("server-main.tsx"))?;
let mut client_entry = fs::File::create(dev_folder.join("client-main.tsx"))?;
server_entry.write_all(SERVER_ENTRY_DATA.as_bytes())?;
client_entry.write_all(CLIENT_ENTRY_DATA.as_bytes())?;
Ok(())
}
#[cfg(test)]
mod tests {
@@ -358,7 +296,13 @@ mod tests {
let mut app = App::new();
app.config = Some(Default::default());
let fallback_html = create_html_fallback(&app);
let source_builder = SourceBuilder {
app,
mode: Mode::Dev,
base_path: PathBuf::new(),
};
let fallback_html = source_builder.build_html_fallback();
assert!(fallback_html.contains("http://localhost:3000/vite-server/@react-refresh"));
assert!(fallback_html.contains("http://localhost:3000/vite-server/@vite/client"));
+9
View File
@@ -0,0 +1,9 @@
// File automatically generated by tuono
// Do not manually update this file
import 'vite/modulepreload-polyfill'
import { hydrate } from 'tuono/hydration'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
hydrate(routeTree)
+19
View File
@@ -0,0 +1,19 @@
<!doctype html>
<html>
<body>
<script>
window['__TUONO_SERVER_PAYLOAD__'] = [SERVER_PAYLOAD]
</script>
<script type="module" async>
import RefreshRuntime from '[BASE_URL]/vite-server/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => { }
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" async src="[BASE_URL]/vite-server/@vite/client"></script>
<script type="module" async src="[BASE_URL]/vite-server/client-main.tsx"></script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
// File automatically generated
// Do not manually change it
use tuono_lib::{tokio, Mode, Server, axum::Router, tuono_internal_init_v8_platform};
// AXUM_GET_ROUTE_HANDLER
const MODE: Mode = /*MODE*/;
// MODULE_IMPORTS
//MAIN_FILE_IMPORT//
#[tokio::main]
async fn main() {
tuono_internal_init_v8_platform();
if MODE == Mode::Prod {
println!("\n ⚡ Tuono v/*VERSION*/");
}
//MAIN_FILE_DEFINITION//
let router = Router::new()
// ROUTE_BUILDER
//MAIN_FILE_USAGE//;
Server::init(router, MODE).await.start().await
}
+7
View File
@@ -0,0 +1,7 @@
// File automatically generated by tuono
// Do not manually update this file
import { serverSideRendering } from 'tuono/ssr'
import { routeTree } from './routeTree.gen'
export const renderFn = serverSideRendering(routeTree)
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_internal"
version = "0.19.0"
version = "0.19.2"
edition = "2024"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "Superfast React fullstack framework"
+1
View File
@@ -1 +1,2 @@
pub mod config;
pub mod tuono_println;
@@ -0,0 +1,11 @@
#[macro_export]
/// Log a message in the terminal using the custom tuono formatter.
/// The messages printed with this macro should inform or
/// guide the user.
///
/// The debug/error messages should be printed using the `tracing` crate
macro_rules! tuono_println {
($($arg:tt)*) => {{
println!(" {}", format!($($arg)*));
}};
}
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib"
version = "0.19.0"
version = "0.19.2"
edition = "2024"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "Superfast React fullstack framework"
@@ -30,10 +30,10 @@ once_cell = "1.19.0"
regex = "1.10.5"
either = "1.13.0"
tower-http = {version = "0.6.0", features = ["fs"]}
colored = "2.1.0"
colored = "3.0.0"
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.19.0"}
tuono_internal = {path = "../tuono_internal", version = "0.19.0"}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.19.2"}
tuono_internal = {path = "../tuono_internal", version = "0.19.2"}
# Match the same version used by axum
tokio-tungstenite = "0.26.0"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
+102 -2
View File
@@ -1,8 +1,8 @@
use axum::http::{HeaderMap, Uri};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use axum::http::{HeaderMap, Uri};
/// Location must match client side interface
#[derive(Serialize, Debug)]
pub struct Location {
@@ -70,12 +70,40 @@ impl Request {
"Failed to read body",
)))
}
pub fn form_data<T>(&self) -> Result<T, BodyParseError>
where
T: DeserializeOwned,
{
let content_type = self
.headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if !content_type.contains("application/x-www-form-urlencoded") {
return Err(BodyParseError::ContentType(
"Invalid content type, expected application/x-www-form-urlencoded".to_string(),
));
}
let body = self.body.as_ref().ok_or_else(|| {
BodyParseError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Missing request body",
))
})?;
serde_urlencoded::from_bytes::<T>(body).map_err(BodyParseError::UrlEncoded)
}
}
#[derive(Debug)]
pub enum BodyParseError {
Io(std::io::Error),
Serde(serde_json::Error),
UrlEncoded(serde_urlencoded::de::Error),
ContentType(String),
}
impl From<serde_json::Error> for BodyParseError {
@@ -95,6 +123,12 @@ mod tests {
field2: String,
}
#[derive(Debug, Deserialize)]
struct FormData {
name: String,
email: Option<String>,
}
#[test]
fn it_correctly_parse_the_body() {
let request = Request::new(
@@ -123,4 +157,70 @@ mod tests {
assert!(body.is_err());
}
#[test]
fn it_correctly_parses_form_data() {
let mut request = Request::new(
Uri::from_static("http://localhost:3000"),
HeaderMap::new(),
HashMap::new(),
None,
);
request.headers.insert(
"content-type",
"application/x-www-form-urlencoded".parse().unwrap(),
);
request.body = Some("name=John+Doe&email=john%40example.com".as_bytes().to_vec());
let form_data: Result<FormData, BodyParseError> = request.form_data();
assert!(form_data.is_ok());
let data = form_data.unwrap();
assert_eq!(data.name, "John Doe");
assert_eq!(data.email, Some("john@example.com".to_string()));
}
#[test]
fn it_rejects_wrong_form_content_type() {
let mut request = Request::new(
Uri::from_static("http://localhost:3000"),
HeaderMap::new(),
HashMap::new(),
None,
);
request
.headers
.insert("content-type", "application/json".parse().unwrap());
request.headers.insert(
"body",
"name=John+Doe&email=john%40example.com".parse().unwrap(),
);
let form_data: Result<FormData, BodyParseError> = request.form_data();
assert!(form_data.is_err());
}
#[test]
fn it_handles_missing_form_body() {
let mut request = Request::new(
Uri::from_static("http://localhost:3000"),
HeaderMap::new(),
HashMap::new(),
None,
);
request.headers.insert(
"content-type",
"application/x-www-form-urlencoded".parse().unwrap(),
);
let form_data: Result<FormData, BodyParseError> = request.form_data();
assert!(form_data.is_err());
}
}
+11 -14
View File
@@ -6,6 +6,7 @@ use colored::Colorize;
use ssr_rs::Ssr;
use tower_http::services::ServeDir;
use tuono_internal::config::Config;
use tuono_internal::tuono_println;
use crate::env::load_env_vars;
use crate::vite::{vite_reverse_proxy, vite_websocket_proxy};
@@ -29,23 +30,19 @@ pub struct Server {
impl Server {
fn display_start_message(&self) {
/*
* Format the server address as a valid URL so that it becomes clickable in the CLI
* @see https://github.com/tuono-labs/tuono/issues/460
*/
// Format the server address as a valid URL so that it becomes clickable in the CLI
// @see https://github.com/tuono-labs/tuono/issues/460
let server_base_url = format!("http://{}", self.address);
if self.mode == Mode::Dev {
println!(" Ready at: {}\n", server_base_url.blue().bold());
// In order to avoid multiple logs on `tuono dev`
// the server address prompt for tuono dev is made on the CLI process
if self.mode == Mode::Prod {
tuono_println!("Production server at: {}\n", server_base_url.blue().bold());
if let Some(origin) = &self.origin {
tuono_println!("Origin: {}\n", origin.blue().bold());
}
} else {
println!(
" Production server at: {}\n",
server_base_url.blue().bold()
);
}
if let Some(origin) = &self.origin {
println!(" Origin: {}\n", origin.blue().bold());
tuono_println!("Ready\n");
}
}
+29
View File
@@ -1,4 +1,6 @@
mod utils;
use std::collections::HashMap;
use crate::utils::mock_server::MockTuonoServer;
use serial_test::serial;
@@ -194,3 +196,30 @@ async fn it_parses_the_http_body() {
assert!(response.status().is_success());
assert_eq!(response.text().await.unwrap(), "payload");
}
#[tokio::test]
#[serial]
async fn it_parses_the_form_encoded_url() {
let app = MockTuonoServer::spawn().await;
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let server_url = format!("http://{}:{}", &app.address, &app.port);
let mut form_params = HashMap::new();
form_params.insert("data", "payload");
let response = client
.post(format!("{server_url}/api/form_data"))
.header("content-type", "application/x-www-form-urlencoded")
.form(&form_params)
.send()
.await
.expect("Failed to execute request.");
assert!(response.status().is_success());
assert_eq!(response.text().await.unwrap(), "payload");
}
+13
View File
@@ -0,0 +1,13 @@
use serde::Deserialize;
use tuono_lib::Request;
#[derive(Deserialize)]
struct Payload {
data: String,
}
#[tuono_lib::api(POST)]
async fn form_data(req: Request) -> String {
let form = req.form_data::<Payload>().unwrap();
form.data
}
@@ -10,6 +10,7 @@ use tuono_lib::{Mode, Server, axum::Router, tuono_internal_init_v8_platform};
use crate::utils::catch_all::get_tuono_internal_api as catch_all;
use crate::utils::dynamic_parameter::get_tuono_internal_api as dynamic_parameter;
use crate::utils::env::get_tuono_internal_api as test_env;
use crate::utils::form_data::post_tuono_internal_api as form_data_api;
use crate::utils::health_check::get_tuono_internal_api as health_check;
use crate::utils::post_api::post_tuono_internal_api as post_api;
use crate::utils::route as html_route;
@@ -86,6 +87,7 @@ impl MockTuonoServer {
.route("/catch_all/{*catch_all}", get(catch_all))
.route("/dynamic/{parameter}", get(dynamic_parameter))
.route("/api/post", post(post_api))
.route("/api/form_data", post(form_data_api))
.route("/env", get(test_env));
let server = Server::init(router, Mode::Prod).await;
+1
View File
@@ -1,6 +1,7 @@
pub mod catch_all;
pub mod dynamic_parameter;
pub mod env;
pub mod form_data;
pub mod health_check;
pub mod mock_server;
pub mod post_api;
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib_macros"
version = "0.19.0"
version = "0.19.2"
edition = "2024"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
+3 -3
View File
@@ -15,10 +15,10 @@
"types": "tsc --noEmit"
},
"devDependencies": {
"oxc-transform": "0.61.2",
"oxc-transform": "0.62.0",
"rollup-plugin-preserve-directives": "0.4.0",
"unplugin-isolated-decl": "0.11.2",
"vite": "6.1.2",
"unplugin-isolated-decl": "0.13.6",
"vite": "6.1.3",
"vite-plugin-externalize-deps": "0.9.0"
}
}
+5 -6
View File
@@ -1,13 +1,12 @@
import type { ReactNode, JSX } from 'react'
import type { JSX } from 'react'
import { TuonoScripts } from 'tuono'
import type { TuonoLayoutProps } from 'tuono'
import '../styles/global.css'
interface RootLayoutProps {
children: ReactNode
}
export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
export default function RootLayout({
children,
}: TuonoLayoutProps): JSX.Element {
return (
<html>
<body>
@@ -1,13 +1,12 @@
import type { ReactNode, JSX } from 'react'
import type { JSX } from 'react'
import { TuonoScripts } from 'tuono'
import type { TuonoLayoutProps } from 'tuono'
import '../styles/global.css'
interface RootLayoutProps {
children: ReactNode
}
export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
export default function RootLayout({
children,
}: TuonoLayoutProps): JSX.Element {
return (
<html>
<body>
+5 -6
View File
@@ -1,14 +1,13 @@
import type { ReactNode, JSX } from 'react'
import type { JSX } from 'react'
import { MDXProvider } from '@mdx-js/react'
import { TuonoScripts } from 'tuono'
import type { TuonoLayoutProps } from 'tuono'
import '../styles/global.css'
interface RootLayoutProps {
children: ReactNode
}
export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
export default function RootLayout({
children,
}: TuonoLayoutProps): JSX.Element {
return (
<html>
<body>
@@ -1,13 +1,12 @@
import type { ReactNode, JSX } from 'react'
import type { JSX } from 'react'
import { TuonoScripts } from 'tuono'
import type { TuonoLayoutProps } from 'tuono'
import '../styles/global.css'
interface RootLayoutProps {
children: ReactNode
}
export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
export default function RootLayout({
children,
}: TuonoLayoutProps): JSX.Element {
return (
<html>
<head>
+12 -12
View File
@@ -2,17 +2,17 @@
"name": "workspace",
"private": true,
"type": "module",
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6",
"packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808",
"scripts": {
"dev": "turbo dev --filter=./devtools/* --filter=./packages/*",
"build": "turbo build --filter=./devtools/* --filter=./packages/*",
"lint": "turbo lint --filter=./devtools/* --filter=./packages/*",
"format:fix": "turbo format --filter=./devtools/* --filter=./packages/*",
"format:fix": "turbo format:fix --filter=./devtools/* --filter=./packages/*",
"format": "turbo format --filter=./devtools/* --filter=./packages/*",
"typecheck": "turbo typecheck --filter=./devtools/* --filter=./packages/*",
"test": "turbo test --filter=./devtools/* --filter=./packages/*",
"test:watch": "turbo test:watch --filter=./devtools/* --filter=./packages/*",
"test:e2e": "pnpm --filter='fixture-*' run test:e2e",
"test:e2e": "pnpm --filter \"./e2e/fixtures/*\" run test:e2e",
"examples:format": "prettier ./examples/** --check",
"examples:format:fix": "prettier ./examples/** --write",
"examples:typecheck": "pnpm --filter \"./examples/*\" exec tsc --noEmit",
@@ -25,18 +25,18 @@
"author": "Valerio Ageno",
"license": "MIT",
"devDependencies": {
"@eslint/js": "9.20.0",
"@eslint/js": "9.24.0",
"@playwright/test": "1.51.1",
"@types/node": "22.13.13",
"@vitest/eslint-plugin": "1.1.36",
"eslint": "9.20.0",
"eslint-import-resolver-typescript": "3.7.0",
"@types/node": "22.14.0",
"@vitest/eslint-plugin": "1.1.39",
"eslint": "9.24.0",
"eslint-import-resolver-typescript": "4.3.2",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "5.1.0",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "5.2.0",
"prettier": "3.5.3",
"turbo": "2.4.4",
"turbo": "2.5.0",
"typescript": "5.7.3",
"typescript-eslint": "8.24.0"
"typescript-eslint": "8.29.0"
}
}
@@ -1,6 +1,6 @@
{
"name": "tuono-fs-router-vite-plugin",
"version": "0.19.0",
"version": "0.19.2",
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
@@ -46,6 +46,6 @@
"devDependencies": {
"@types/babel__core": "7.20.5",
"vite-config": "workspace:*",
"vitest": "3.0.9"
"vitest": "3.1.1"
}
}
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-router",
"version": "0.19.0",
"version": "0.19.2",
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
@@ -44,13 +44,13 @@
"react-intersection-observer": "^9.13.0"
},
"devDependencies": {
"@testing-library/react": "16.2.0",
"@types/react": "19.0.10",
"@vitejs/plugin-react-swc": "3.8.0",
"@testing-library/react": "16.3.0",
"@types/react": "19.1.0",
"@vitejs/plugin-react-swc": "3.8.1",
"happy-dom": "17.4.4",
"react": "19.0.0",
"vite": "6.1.2",
"react": "19.1.0",
"vite": "6.1.3",
"vite-config": "workspace:*",
"vitest": "3.0.9"
"vitest": "3.1.1"
}
}
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "tuono",
"version": "0.19.0",
"version": "0.19.2",
"description": "Superfast React fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
@@ -79,14 +79,14 @@
},
"devDependencies": {
"@types/babel__core": "7.20.5",
"@types/babel__traverse": "7.20.6",
"@types/node": "22.13.13",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"react": "19.0.0",
"react-dom": "19.0.0",
"@types/babel__traverse": "7.20.7",
"@types/node": "22.14.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"vite-config": "workspace:*",
"vitest": "3.0.9"
"vitest": "3.1.1"
},
"sideEffects": false,
"keywords": [
+2 -2
View File
@@ -1,5 +1,5 @@
import type { TuonoConfig } from '../config'
import type { TuonoConfig, TuonoConfigServer } from '../config'
export interface InternalTuonoConfig extends Omit<TuonoConfig, 'server'> {
server: Required<NonNullable<TuonoConfig['server']>>
server: TuonoConfigServer
}
+1 -1
View File
@@ -1 +1 @@
export type { TuonoConfig } from './types'
export type { TuonoConfig, TuonoConfigServer } from './types'
+7 -5
View File
@@ -5,15 +5,17 @@ import type {
CSSOptions,
} from 'vite'
export interface TuonoConfigServer {
host: string
origin: string | null
port: number
}
/**
* @see http://tuono.dev/documentation/configuration
*/
export interface TuonoConfig {
server?: {
host?: string
origin?: string | null
port?: number
}
server?: Partial<TuonoConfigServer>
vite?: {
alias?: AliasOptions
css?: CSSOptions
+1 -1
View File
@@ -13,4 +13,4 @@ export {
export { TuonoScripts } from './shared/TuonoScripts'
export type { TuonoRouteProps } from './types'
export type { TuonoRouteProps, TuonoLayoutProps } from './types'
+8 -3
View File
@@ -1,12 +1,17 @@
import type { JSX } from 'react'
import { useTuonoContextServerPayload } from './TuonoContext'
import type { TuonoConfigServer } from '../config'
const VITE_PROXY_PATH = '/vite-server'
const DEFAULT_SERVER_CONFIG = { host: 'localhost', origin: null, port: 3000 }
export const DevResources = (): JSX.Element => {
const { devServerConfig } = useTuonoContextServerPayload()
interface DevResourcesProps {
devServerConfig?: TuonoConfigServer
}
export const DevResources = ({
devServerConfig,
}: DevResourcesProps): JSX.Element => {
const { host, origin, port } = devServerConfig ?? DEFAULT_SERVER_CONFIG
const viteBaseUrl =
+8 -4
View File
@@ -1,10 +1,14 @@
import type { JSX } from 'react'
import { useTuonoContextServerPayload } from './TuonoContext'
export const ProdResources = (): JSX.Element => {
const { cssBundles, jsBundles } = useTuonoContextServerPayload()
interface ProdResourcesProps {
jsBundles: Array<string> | null
cssBundles: Array<string> | null
}
export const ProdResources = ({
cssBundles,
jsBundles,
}: ProdResourcesProps): JSX.Element => {
return (
<>
{cssBundles?.map((cssHref) => (
+9 -2
View File
@@ -12,8 +12,15 @@ export function TuonoScripts(): JSX.Element {
return (
<>
<script>{`window['${SERVER_PAYLOAD_VARIABLE_NAME}']=${JSON.stringify(serverPayload)}`}</script>
{serverPayload.mode === 'Dev' && <DevResources />}
{serverPayload.mode === 'Prod' && <ProdResources />}
{serverPayload.mode === 'Dev' && (
<DevResources devServerConfig={serverPayload.devServerConfig} />
)}
{serverPayload.mode === 'Prod' && (
<ProdResources
jsBundles={serverPayload.jsBundles}
cssBundles={serverPayload.cssBundles}
/>
)}
</>
)
}
+11 -23
View File
@@ -1,3 +1,7 @@
import type { ReactNode } from 'react'
import type { TuonoConfigServer } from './config'
/**
* Provided by the rust server and used in the ssr env
* @see tuono-router {@link ServerInitialLocation}
@@ -11,27 +15,11 @@ export interface ServerPayloadLocation {
/**
* @see crates/tuono_lib/src/payload.rs
*/
export interface ServerPayload<TData = unknown> {
mode: 'Prod' | 'Dev'
export type ServerPayload<TData = unknown> = {
location: ServerPayloadLocation
data: TData
/** Available only on 'Prod' mode */
jsBundles: Array<string> | null
cssBundles: Array<string> | null
/** Available only on 'Dev' mode */
devServerConfig?: {
port: number
origin: string | null
host: string
}
}
/* the above type could be refined with an union like this
(
} & (
| {
mode: 'Prod'
jsBundles: Array<string>
@@ -39,13 +27,9 @@ export interface ServerPayload<TData = unknown> {
}
| {
mode: 'Dev'
devServerConfig: {
port: number
host: string
}
devServerConfig?: TuonoConfigServer
}
)
*/
export type TuonoRouteProps<TData> =
| {
@@ -56,3 +40,7 @@ export type TuonoRouteProps<TData> =
data: TData
isLoading: false
}
export interface TuonoLayoutProps {
children: ReactNode
}
+964 -714
View File
File diff suppressed because it is too large Load Diff