mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
Merge remote-tracking branch 'origin/main' into remove-windows-vite-log-warning
This commit is contained in:
@@ -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')) }}
|
||||
|
||||
@@ -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')) }}
|
||||
|
||||
@@ -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')) }}
|
||||
|
||||
@@ -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
@@ -1,9 +1,2 @@
|
||||
* @Valerioageno @marcalexiei
|
||||
|
||||
# Rust
|
||||
/crates/ @Valerioageno
|
||||
/Cargo.toml @Valerioageno
|
||||
|
||||
# Misc
|
||||
/.github/ @marcalexiei
|
||||
* @Valerioageno
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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! 🎉");
|
||||
|
||||
|
||||
@@ -7,5 +7,6 @@ mod app;
|
||||
pub mod cli;
|
||||
mod commands;
|
||||
mod mode;
|
||||
mod process_manager;
|
||||
mod route;
|
||||
mod source_builder;
|
||||
|
||||
@@ -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(),
|
||||
}))
|
||||
}
|
||||
@@ -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"));
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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 +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)*));
|
||||
}};
|
||||
}
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,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,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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 @@
|
||||
export type { TuonoConfig } from './types'
|
||||
export type { TuonoConfig, TuonoConfigServer } from './types'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,4 +13,4 @@ export {
|
||||
|
||||
export { TuonoScripts } from './shared/TuonoScripts'
|
||||
|
||||
export type { TuonoRouteProps } from './types'
|
||||
export type { TuonoRouteProps, TuonoLayoutProps } from './types'
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Generated
+964
-714
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user