mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-27 13:52:47 -07:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f6bad637c | |||
| 3063763667 | |||
| caf1a58a0b | |||
| 059f3d6476 | |||
| 303a177344 | |||
| d26918fc2f | |||
| 31f0a167e9 | |||
| d71185c9cb | |||
| 798f8f1bf8 | |||
| 06612f07b0 | |||
| 067535c2f6 | |||
| d679982949 | |||
| de5ac53cc3 | |||
| ca4a7b3466 | |||
| 0a4d1e2995 | |||
| d6fc18e2f6 | |||
| 8e04abbe42 | |||
| 2905cfae3d | |||
| b632cf43ba | |||
| 06bc3700b7 | |||
| d4648afe01 | |||
| 0653349c08 | |||
| 12e0937174 | |||
| d37b655add | |||
| e3b1c0e013 |
@@ -5,3 +5,6 @@ end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 80
|
||||
|
||||
[*.rs]
|
||||
indent_size = 4
|
||||
|
||||
@@ -8,6 +8,7 @@ typescript:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
[
|
||||
'devtools/**',
|
||||
'packages/**',
|
||||
'package.json',
|
||||
'pnpm-*.yaml',
|
||||
@@ -23,6 +24,7 @@ typescript:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
[
|
||||
'devtools/**',
|
||||
'.npmrc',
|
||||
'.nvmrc',
|
||||
'.prettierrc',
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Test project
|
||||
run: pnpm repo:root:format:check
|
||||
run: pnpm repo:root:format
|
||||
|
||||
ci_ok:
|
||||
name: Repo root CI OK
|
||||
|
||||
@@ -51,13 +51,13 @@ jobs:
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm format:check
|
||||
run: pnpm run format
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Types
|
||||
run: pnpm types
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
ci_ok:
|
||||
name: Typescript CI OK
|
||||
|
||||
@@ -6,3 +6,5 @@ dist
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
packages/tuono-lazy-fn-vite-plugin/tests/sources/*
|
||||
|
||||
e2e/fixtures/*/test-results
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.17.8"
|
||||
version = "0.17.9"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "Superfast React fullstack framework"
|
||||
@@ -19,6 +19,8 @@ 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"
|
||||
watchexec-signals = "4.0.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
@@ -30,11 +32,13 @@ 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.17.8"}
|
||||
tuono_internal = {path = "../tuono_internal", version = "0.17.9"}
|
||||
spinners = "4.1.1"
|
||||
console = "0.15.10"
|
||||
|
||||
[dev-dependencies]
|
||||
wiremock = "0.6.2"
|
||||
tempfile = "3.14.0"
|
||||
assert_cmd = "2.0.16"
|
||||
serial_test = "0.10.0"
|
||||
|
||||
|
||||
+26
-13
@@ -5,6 +5,7 @@ use http::Method;
|
||||
use std::collections::hash_set::HashSet;
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
@@ -12,6 +13,7 @@ use std::path::PathBuf;
|
||||
use std::process::Child;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use tracing::error;
|
||||
use tuono_internal::config::Config;
|
||||
|
||||
use crate::route::Route;
|
||||
@@ -176,15 +178,18 @@ impl App {
|
||||
|
||||
pub fn build_react_prod(&self) {
|
||||
if !Path::new(BUILD_JS_SCRIPT).exists() {
|
||||
eprintln!("Failed to find the build script. Please run `npm install`");
|
||||
error!("Failed to find the build script. Please run `npm install`");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let output = Command::new(BUILD_JS_SCRIPT)
|
||||
.output()
|
||||
.expect("Failed to build the react source");
|
||||
|
||||
let output = Command::new(BUILD_JS_SCRIPT).output().unwrap_or_else(|_| {
|
||||
error!("Failed to build the react source");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!("Failed to build the react source");
|
||||
eprintln!("Error: {}", String::from_utf8_lossy(&output.stderr));
|
||||
error!("Failed to build the react source");
|
||||
error!("Error: {}", String::from_utf8_lossy(&output.stderr));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -204,18 +209,26 @@ impl App {
|
||||
eprintln!("Failed to find the build script. Please run `npm install`");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let config_build_command = Command::new(BUILD_TUONO_CONFIG)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output();
|
||||
|
||||
if let Ok(config) = Config::get() {
|
||||
self.config = Some(config);
|
||||
} else {
|
||||
eprintln!("[CLI] Failed to read tuono.config.ts");
|
||||
std::process::exit(1);
|
||||
};
|
||||
match Config::get() {
|
||||
Ok(config) => self.config = Some(config),
|
||||
Err(error) => {
|
||||
match error.kind() {
|
||||
io::ErrorKind::NotFound => eprintln!("Failed to read config. Please run `npm install` to generate automatically."),
|
||||
_ => {
|
||||
error!("Failed to read config with the following error:");
|
||||
error!("{}", error);
|
||||
}
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
config_build_command
|
||||
}
|
||||
@@ -349,7 +362,7 @@ mod tests {
|
||||
("/about", "/about"),
|
||||
("/posts/index", "/posts"),
|
||||
("/posts/any-post", "/posts/any-post"),
|
||||
("/posts/[post]", "/posts/:post"),
|
||||
("/posts/[post]", "/posts/{post}"),
|
||||
];
|
||||
|
||||
results.into_iter().for_each(|(path, expected_path)| {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
use fs_extra::dir::{copy, CopyOptions};
|
||||
use spinners::{Spinner, Spinners};
|
||||
use std::path::PathBuf;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use tracing::{error, trace};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::mode::Mode;
|
||||
|
||||
fn exit_gracefully_with_error(msg: &str) -> ! {
|
||||
error!(msg);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
pub fn build(mut app: App, ssg: bool, no_js_emit: bool) {
|
||||
if no_js_emit {
|
||||
println!("Rust build successfully finished");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
if ssg && app.has_dynamic_routes() {
|
||||
// TODO: allow dynamic routes static generation
|
||||
println!("Cannot statically build dynamic routes");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
app.build_tuono_config()
|
||||
.unwrap_or_else(|_| exit_gracefully_with_error("Failed to build tuono.config.ts"));
|
||||
|
||||
let mut app_build_spinner = Spinner::new(Spinners::Dots, "Building app...".into());
|
||||
|
||||
app.check_server_availability(Mode::Prod);
|
||||
|
||||
app.build_react_prod();
|
||||
|
||||
// Remove the spinner
|
||||
app_build_spinner.stop_with_message("\u{2705}Build completed".into());
|
||||
|
||||
if ssg {
|
||||
let mut app_build_static_spinner =
|
||||
Spinner::new(Spinners::Dots, "Static site generation".into());
|
||||
|
||||
let mut exit_gracefully_with_error = |msg: &str| -> ! {
|
||||
app_build_static_spinner.stop_with_message("\u{274C} Build failed\n".into());
|
||||
exit_gracefully_with_error(msg)
|
||||
};
|
||||
|
||||
let static_dir = PathBuf::from("out/static");
|
||||
|
||||
if static_dir.is_dir() {
|
||||
std::fs::remove_dir_all(&static_dir).unwrap_or_else(|_| {
|
||||
exit_gracefully_with_error("Failed to clear the out/static folder")
|
||||
});
|
||||
}
|
||||
|
||||
std::fs::create_dir(&static_dir)
|
||||
.unwrap_or_else(|_| exit_gracefully_with_error("Failed to create static output dir"));
|
||||
|
||||
copy(
|
||||
"./out/client",
|
||||
static_dir,
|
||||
&CopyOptions::new().overwrite(true).content_only(true),
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
exit_gracefully_with_error("Failed to clone assets into static output folder")
|
||||
});
|
||||
|
||||
// Start the server
|
||||
#[allow(clippy::zombie_processes)]
|
||||
let mut rust_server = app.run_rust_server();
|
||||
|
||||
let mut exit_and_shut_server = |msg: &str| -> ! {
|
||||
_ = rust_server.kill();
|
||||
exit_gracefully_with_error(msg)
|
||||
};
|
||||
|
||||
let reqwest_client = reqwest::blocking::Client::builder()
|
||||
.user_agent("")
|
||||
.build()
|
||||
.unwrap_or_else(|_| exit_and_shut_server("Failed to build reqwest client"));
|
||||
|
||||
// Wait for server
|
||||
let mut is_server_ready = false;
|
||||
let config = app.config.as_ref().unwrap();
|
||||
|
||||
while !is_server_ready {
|
||||
trace!("Checking server availability");
|
||||
let server_url = format!("http://{}:{}", config.server.host, config.server.port);
|
||||
if reqwest_client.get(&server_url).send().is_ok() {
|
||||
is_server_ready = true;
|
||||
} else {
|
||||
trace!("Server not ready yet. Sleeping for 1 second");
|
||||
}
|
||||
sleep(Duration::from_secs(1));
|
||||
}
|
||||
|
||||
trace!("Server is ready, starting static site generation");
|
||||
|
||||
for (_, route) in app.route_map {
|
||||
if let Err(msg) = route.save_ssg_file(&reqwest_client) {
|
||||
exit_and_shut_server(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Close server
|
||||
let _ = rust_server.kill();
|
||||
|
||||
app_build_static_spinner
|
||||
.stop_with_message("\u{2705}Static site generation completed".into());
|
||||
}
|
||||
}
|
||||
+21
-72
@@ -1,11 +1,11 @@
|
||||
use fs_extra::dir::{copy, CopyOptions};
|
||||
use std::path::PathBuf;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use tracing::{span, Level};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::build;
|
||||
use crate::mode::Mode;
|
||||
use crate::scaffold_project;
|
||||
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
|
||||
@@ -63,10 +63,20 @@ fn init_tuono_folder(mode: Mode) -> std::io::Result<App> {
|
||||
}
|
||||
|
||||
pub fn app() -> std::io::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
// Not need for the time since the code is synchronous
|
||||
.without_time()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
match args.action {
|
||||
Actions::Dev => {
|
||||
let span = span!(Level::TRACE, "DEV");
|
||||
|
||||
let _guard = span.enter();
|
||||
|
||||
let mut app = init_tuono_folder(Mode::Dev)?;
|
||||
|
||||
app.build_tuono_config()
|
||||
@@ -77,84 +87,23 @@ pub fn app() -> std::io::Result<()> {
|
||||
watch::watch().unwrap();
|
||||
}
|
||||
Actions::Build { ssg, no_js_emit } => {
|
||||
let mut app = init_tuono_folder(Mode::Prod)?;
|
||||
let span = span!(Level::TRACE, "BUILD");
|
||||
|
||||
if no_js_emit {
|
||||
println!("Rust build successfully finished");
|
||||
return Ok(());
|
||||
}
|
||||
let _guard = span.enter();
|
||||
|
||||
if ssg && app.has_dynamic_routes() {
|
||||
// TODO: allow dynamic routes static generation
|
||||
println!("Cannot statically build dynamic routes");
|
||||
return Ok(());
|
||||
}
|
||||
let app = init_tuono_folder(Mode::Prod)?;
|
||||
|
||||
app.build_tuono_config()
|
||||
.expect("Failed to build tuono.config.ts");
|
||||
|
||||
app.check_server_availability(Mode::Prod);
|
||||
|
||||
app.build_react_prod();
|
||||
|
||||
if ssg {
|
||||
println!("SSG: generation started");
|
||||
|
||||
let static_dir = PathBuf::from("out/static");
|
||||
|
||||
if static_dir.is_dir() {
|
||||
std::fs::remove_dir_all(&static_dir)
|
||||
.expect("Failed to clear the out/static folder");
|
||||
}
|
||||
|
||||
std::fs::create_dir(&static_dir).expect("Failed to create static output dir");
|
||||
|
||||
copy(
|
||||
"./out/client",
|
||||
static_dir,
|
||||
&CopyOptions::new().overwrite(true).content_only(true),
|
||||
)
|
||||
.expect("Failed to clone assets into static output folder");
|
||||
|
||||
// the process is killed below so we don't really need to use wait() function
|
||||
#[allow(clippy::zombie_processes)]
|
||||
let mut rust_server = app.run_rust_server();
|
||||
|
||||
let reqwest = reqwest::blocking::Client::builder()
|
||||
.user_agent("")
|
||||
.build()
|
||||
.expect("Failed to build reqwest client");
|
||||
|
||||
// Wait for server
|
||||
let mut is_server_ready = false;
|
||||
|
||||
let config = app.config.as_ref().unwrap();
|
||||
|
||||
while !is_server_ready {
|
||||
let server_url =
|
||||
format!("http://{}:{}", config.server.host, config.server.port);
|
||||
if reqwest.get(server_url).send().is_ok() {
|
||||
is_server_ready = true
|
||||
}
|
||||
// TODO: add maximum tries
|
||||
sleep(Duration::from_secs(1))
|
||||
}
|
||||
|
||||
for (_, route) in app.route_map {
|
||||
route.save_ssg_file(&reqwest)
|
||||
}
|
||||
|
||||
// Close server
|
||||
let _ = rust_server.kill();
|
||||
};
|
||||
|
||||
println!("Build successfully finished");
|
||||
build::build(app, ssg, no_js_emit);
|
||||
}
|
||||
Actions::New {
|
||||
folder_name,
|
||||
template,
|
||||
head,
|
||||
} => {
|
||||
let span = span!(Level::TRACE, "NEW");
|
||||
|
||||
let _guard = span.enter();
|
||||
|
||||
scaffold_project::create_new_project(folder_name, template, head);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! You can find the full documentation at [tuono.dev](https://tuono.dev/)
|
||||
|
||||
mod app;
|
||||
mod build;
|
||||
pub mod cli;
|
||||
mod mode;
|
||||
mod route;
|
||||
|
||||
+94
-25
@@ -7,6 +7,7 @@ use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use tracing::trace;
|
||||
|
||||
fn has_dynamic_path(route: &str) -> bool {
|
||||
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
|
||||
@@ -45,6 +46,20 @@ impl AxumInfo {
|
||||
}
|
||||
|
||||
if route.is_dynamic {
|
||||
let dyn_re = Regex::new(r"\[(.*?)\]").expect("Failed to create dyn regex");
|
||||
let catch_all_re =
|
||||
Regex::new(r"\{\.\.\.(.*?)\}").expect("Failed to create catch all regex");
|
||||
|
||||
let dyn_result = dyn_re.replace_all(&axum_route, |caps: ®ex::Captures| {
|
||||
format!("{{{}}}", &caps[1])
|
||||
});
|
||||
|
||||
let axum_route = catch_all_re
|
||||
.replace_all(&dyn_result, |caps: ®ex::Captures| {
|
||||
format!("{{*{}}}", &caps[1])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
return AxumInfo {
|
||||
module_import: module
|
||||
.as_str()
|
||||
@@ -54,10 +69,7 @@ impl AxumInfo {
|
||||
.replace('[', "dyn_")
|
||||
.replace("...", "catch_all_")
|
||||
.replace(']', ""),
|
||||
axum_route: axum_route
|
||||
.replace("[...", "*")
|
||||
.replace('[', ":")
|
||||
.replace(']', ""),
|
||||
axum_route,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,7 +81,7 @@ impl AxumInfo {
|
||||
}
|
||||
|
||||
// TODO: to be extended with common scenarios
|
||||
const NO_HTML_EXTENSIONS: [&str; 1] = ["xml"];
|
||||
const NO_HTML_EXTENSIONS: [&str; 2] = ["xml", "txt"];
|
||||
|
||||
// TODO: Refine this function to catch
|
||||
// if the methods are commented.
|
||||
@@ -141,35 +153,72 @@ impl Route {
|
||||
self.axum_info = Some(AxumInfo::new(self))
|
||||
}
|
||||
|
||||
pub fn save_ssg_file(&self, reqwest: &Client) {
|
||||
pub fn save_ssg_file(&self, reqwest: &Client) -> Result<(), String> {
|
||||
if self.is_api() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let path = &self.path.replace("index", "");
|
||||
|
||||
let mut response = reqwest
|
||||
.get(format!("http://localhost:3000{path}"))
|
||||
.send()
|
||||
.unwrap();
|
||||
let url = format!("http://localhost:3000{path}");
|
||||
|
||||
trace!("Requesting the page: {}", url);
|
||||
let mut response = match reqwest.get(format!("http://localhost:3000{path}")).send() {
|
||||
Ok(response) => response,
|
||||
Err(_) => return Err(format!("Failed to get the response: {}", url)),
|
||||
};
|
||||
|
||||
let file_path = self.output_file_path();
|
||||
|
||||
let parent_dir = file_path.parent().unwrap();
|
||||
let parent_dir = match file_path.parent() {
|
||||
Some(parent_dir) => parent_dir,
|
||||
None => {
|
||||
return Err(format!(
|
||||
"Failed to get the parent directory {:?}",
|
||||
file_path
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if !parent_dir.is_dir() {
|
||||
create_all(parent_dir, false).expect("Failed to create parent directories");
|
||||
if !parent_dir.is_dir() || create_all(parent_dir, false).is_err() {
|
||||
return Err(format!(
|
||||
"Failed to create the parent directory {:?}",
|
||||
parent_dir
|
||||
));
|
||||
}
|
||||
|
||||
let mut file = File::create(file_path).expect("Failed to create the HTML file");
|
||||
trace!("Saving the HTML file: {:?}", file_path);
|
||||
|
||||
io::copy(&mut response, &mut file).expect("Failed to write the HTML on the file");
|
||||
let mut file = match File::create(&file_path) {
|
||||
Ok(file) => file,
|
||||
Err(_) => return Err(format!("Failed to create the file: {:?}", file_path)),
|
||||
};
|
||||
|
||||
if io::copy(&mut response, &mut file).is_err() {
|
||||
return Err(format!("Failed to write the file: {:?}", file_path));
|
||||
}
|
||||
|
||||
// Saving also the server response
|
||||
if self.axum_info.is_some() {
|
||||
let data_file_path = PathBuf::from(&format!("out/static/__tuono/data{path}"));
|
||||
trace!("The route is an axum route, saving the JSON file");
|
||||
|
||||
let data_parent_dir = data_file_path.parent().unwrap();
|
||||
let data_file_path = PathBuf::from(&format!("out/static/__tuono/data{path}.json"));
|
||||
|
||||
if !data_parent_dir.is_dir() {
|
||||
create_all(data_parent_dir, false)
|
||||
.expect("Failed to create data parent directories");
|
||||
let data_parent_dir = match data_file_path.parent() {
|
||||
Some(parent_dir) => parent_dir,
|
||||
None => {
|
||||
return Err(format!(
|
||||
"Failed to get the parent directory {:?}",
|
||||
data_file_path
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if !data_parent_dir.is_dir() && create_all(data_parent_dir, false).is_err() {
|
||||
return Err(format!(
|
||||
"Failed to create the parent directory {:?}",
|
||||
data_parent_dir
|
||||
));
|
||||
}
|
||||
|
||||
let base = Url::parse("http://localhost:3000/__tuono/data").unwrap();
|
||||
@@ -182,13 +231,32 @@ impl Route {
|
||||
.join(pathname)
|
||||
.expect("Failed to build the reqwest URL");
|
||||
|
||||
let mut response = reqwest.get(url).send().unwrap();
|
||||
trace!("Requesting the JSON file: {}", url);
|
||||
|
||||
let mut data_file =
|
||||
File::create(data_file_path).expect("Failed to create the JSON file");
|
||||
let mut response = match reqwest.get(url.clone()).send() {
|
||||
Ok(response) => {
|
||||
trace!("Successfully got the response for: {url}");
|
||||
response
|
||||
}
|
||||
Err(_) => return Err(format!("Failed to get the response: {url}")),
|
||||
};
|
||||
|
||||
io::copy(&mut response, &mut data_file).expect("Failed to write the JSON on the file");
|
||||
let mut data_file = match File::create(&data_file_path) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
return Err(format!(
|
||||
"Failed to create the JSON file: {:?}",
|
||||
data_file_path
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if io::copy(&mut response, &mut data_file).is_err() {
|
||||
return Err("Failed to write the JSON file".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_file_path(&self) -> PathBuf {
|
||||
@@ -239,7 +307,7 @@ mod tests {
|
||||
|
||||
let dyn_info = AxumInfo::new(&Route::new("/[posts]".to_string()));
|
||||
|
||||
assert_eq!(dyn_info.axum_route, "/:posts");
|
||||
assert_eq!(dyn_info.axum_route, "/{posts}");
|
||||
assert_eq!(dyn_info.module_import, "dyn_posts");
|
||||
}
|
||||
|
||||
@@ -249,6 +317,7 @@ mod tests {
|
||||
("/index", "out/static/index.html"),
|
||||
("/documentation", "out/static/documentation/index.html"),
|
||||
("/sitemap.xml", "out/static/sitemap.xml"),
|
||||
("/robot.txt", "out/static/robot.txt"),
|
||||
(
|
||||
"/documentation/routing",
|
||||
"out/static/documentation/routing/index.html",
|
||||
|
||||
@@ -7,13 +7,6 @@ use std::fs::{self, create_dir, File, OpenOptions};
|
||||
use std::io::{self, prelude::*};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const GITHUB_TUONO_TAGS_URL: &str = "https://api.github.com/repos/tuono-labs/tuono/git/ref/tags/";
|
||||
|
||||
const GITHUB_TUONO_TAG_COMMIT_TREES_URL: &str =
|
||||
"https://api.github.com/repos/tuono-labs/tuono/git/trees/";
|
||||
|
||||
const GITHUB_RAW_CONTENT_URL: &str = "https://raw.githubusercontent.com/tuono-labs/tuono";
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
enum GithubFileType {
|
||||
#[serde(rename = "blob")]
|
||||
@@ -69,6 +62,12 @@ pub fn create_new_project(
|
||||
) {
|
||||
let folder = folder_name.unwrap_or(".".to_string());
|
||||
|
||||
let github_api_base_url =
|
||||
env::var("__INTERNAL_TUONO_TEST").unwrap_or("https://api.github.com".to_string());
|
||||
|
||||
let github_raw_base_url = env::var("__INTERNAL_TUONO_TEST")
|
||||
.unwrap_or("https://raw.githubusercontent.com".to_string());
|
||||
|
||||
// In case of missing select the tuono example
|
||||
let template = template.unwrap_or("tuono-app".to_string());
|
||||
let client = blocking::Client::builder()
|
||||
@@ -79,7 +78,8 @@ pub fn create_new_project(
|
||||
// This string does not include the "v" version prefix
|
||||
let cli_version: &str = crate_version!();
|
||||
|
||||
let tree_url: String = generate_tree_url(select_head, &client, cli_version);
|
||||
let tree_url: String =
|
||||
generate_tree_url(select_head, &client, cli_version, &github_api_base_url);
|
||||
|
||||
let res_tree = client
|
||||
.get(tree_url)
|
||||
@@ -126,7 +126,8 @@ pub fn create_new_project(
|
||||
} in new_project_files.iter()
|
||||
{
|
||||
if let GithubFileType::Blob = element_type {
|
||||
let url = generate_raw_content_url(select_head, cli_version, path);
|
||||
let url =
|
||||
generate_raw_content_url(select_head, cli_version, path, &github_raw_base_url);
|
||||
|
||||
let file_content = client
|
||||
.get(url)
|
||||
@@ -144,7 +145,7 @@ pub fn create_new_project(
|
||||
let file_path = folder_path.join(&path);
|
||||
|
||||
if let Err(err) = create_file(file_path, file_content) {
|
||||
exit_with_error(&format!("Failed to create file: {}", err));
|
||||
exit_with_error(&format!("Failed to create file: {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,22 +155,34 @@ pub fn create_new_project(
|
||||
outro(folder);
|
||||
}
|
||||
|
||||
fn generate_raw_content_url(select_head: Option<bool>, cli_version: &str, path: &String) -> String {
|
||||
fn generate_raw_content_url(
|
||||
select_head: Option<bool>,
|
||||
cli_version: &str,
|
||||
path: &String,
|
||||
url: &str,
|
||||
) -> String {
|
||||
let tag = if select_head.unwrap_or(false) {
|
||||
"main"
|
||||
"/main"
|
||||
} else {
|
||||
&format!("v{cli_version}")
|
||||
&format!("/tuono-labs/tuono/v{cli_version}")
|
||||
};
|
||||
format!("{}/{}/{}", GITHUB_RAW_CONTENT_URL, tag, path)
|
||||
format!("{url}{tag}/{path}")
|
||||
}
|
||||
|
||||
fn generate_tree_url(select_head: Option<bool>, client: &Client, cli_version: &str) -> String {
|
||||
fn generate_tree_url(
|
||||
select_head: Option<bool>,
|
||||
client: &Client,
|
||||
cli_version: &str,
|
||||
url: &str,
|
||||
) -> String {
|
||||
if select_head.unwrap_or(false) {
|
||||
format!("{}main?recursive=1", GITHUB_TUONO_TAG_COMMIT_TREES_URL)
|
||||
format!("{url}/repos/tuono-labs/tuono/git/trees/main?recursive=1")
|
||||
} else {
|
||||
// This string does not include the "v" version prefix
|
||||
let res_tag = client
|
||||
.get(format!("{}v{}", GITHUB_TUONO_TAGS_URL, cli_version))
|
||||
.get(format!(
|
||||
"{url}/repos/tuono-labs/tuono/git/ref/tags/v{cli_version}"
|
||||
))
|
||||
.send()
|
||||
.unwrap_or_else(|_| {
|
||||
exit_with_error("Failed to call the tag github API for v{cli_version}")
|
||||
@@ -178,8 +191,8 @@ fn generate_tree_url(select_head: Option<bool>, client: &Client, cli_version: &s
|
||||
.unwrap_or_else(|_| exit_with_error("Failed to parse the tag response"));
|
||||
|
||||
format!(
|
||||
"{}{}?recursive=1",
|
||||
GITHUB_TUONO_TAG_COMMIT_TREES_URL, res_tag.object.sha
|
||||
"{url}/repos/tuono-labs/tuono/git/trees/{}?recursive=1",
|
||||
res_tag.object.sha
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -197,7 +210,10 @@ fn create_directories(
|
||||
let path = PathBuf::from(&path.replace(&format!("examples/{template}/"), ""));
|
||||
|
||||
let dir_path = folder_path.join(&path);
|
||||
create_dir(&dir_path).unwrap();
|
||||
if let Err(e) = create_dir(&dir_path) {
|
||||
eprintln!("Failed to create directory {}: {}", dir_path.display(), e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -267,12 +283,13 @@ mod tests {
|
||||
fn generate_valid_content_url_from_head() {
|
||||
let expected = format!(
|
||||
"{}/{}/{}",
|
||||
GITHUB_RAW_CONTENT_URL, "main", "examples/tuono-app"
|
||||
"http://localhost:3000", "main", "examples/tuono-app"
|
||||
);
|
||||
let generated = generate_raw_content_url(
|
||||
Some(true),
|
||||
crate_version!(),
|
||||
&String::from("examples/tuono-app"),
|
||||
"http://localhost:3000",
|
||||
);
|
||||
assert_eq!(expected, generated)
|
||||
}
|
||||
@@ -281,14 +298,15 @@ mod tests {
|
||||
fn generate_valid_content_url_from_cli_version() {
|
||||
let expected = format!(
|
||||
"{}/{}/{}",
|
||||
GITHUB_RAW_CONTENT_URL,
|
||||
&format!("v{}", crate_version!()),
|
||||
"http://localhost:3000",
|
||||
&format!("tuono-labs/tuono/v{}", crate_version!()),
|
||||
"examples/tuono-app"
|
||||
);
|
||||
let generated = generate_raw_content_url(
|
||||
Some(false),
|
||||
crate_version!(),
|
||||
&String::from("examples/tuono-app"),
|
||||
"http://localhost:3000",
|
||||
);
|
||||
assert_eq!(expected, generated)
|
||||
}
|
||||
|
||||
@@ -85,17 +85,17 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
|
||||
|
||||
if !route.is_api() {
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("{axum_route}", get({module_import}::tuono__internal__route))"#
|
||||
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))"#
|
||||
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))"#
|
||||
r#".route("{axum_route}", {method}({module_import}::{method}_tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -127,9 +127,7 @@ pub fn bundle_axum_source(mode: Mode) -> io::Result<App> {
|
||||
let base_path = std::env::current_dir().unwrap();
|
||||
|
||||
let app = App::new();
|
||||
|
||||
let bundled_file = generate_axum_source(&app, mode);
|
||||
|
||||
create_main_file(&base_path, &bundled_file);
|
||||
|
||||
Ok(app)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
mod utils;
|
||||
use assert_cmd::Command;
|
||||
use serial_test::serial;
|
||||
use std::fs;
|
||||
use utils::TempTuonoProject;
|
||||
|
||||
mod utils;
|
||||
use utils::temp_tuono_project::TempTuonoProject;
|
||||
|
||||
const POST_API_FILE: &str = r"#[tuono_lib::api(POST)]";
|
||||
const GET_API_FILE: &str = r"#[tuono_lib::api(GET)]";
|
||||
@@ -36,7 +37,7 @@ fn it_successfully_create_the_index_route() {
|
||||
assert!(temp_main_rs_content.contains("mod index;"));
|
||||
|
||||
assert!(temp_main_rs_content
|
||||
.contains(r#".route("/", get(index::tuono__internal__route)).route("/__tuono/data/", get(index::tuono__internal__api))"#));
|
||||
.contains(r#".route("/", get(index::tuono_internal_route)).route("/__tuono/data/", get(index::tuono_internal_api))"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -58,13 +59,11 @@ fn it_successfully_create_an_api_route() {
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
dbg!(&temp_main_rs_content);
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/api/health_check.rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod api_health_check;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/health_check", post(api_health_check::post__tuono_internal_api))"#
|
||||
r#".route("/api/health_check", post(api_health_check::post_tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
|
||||
@@ -94,11 +93,10 @@ fn it_successfully_create_multiple_api_for_the_same_file() {
|
||||
assert!(temp_main_rs_content.contains("mod api_health_check;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/health_check", post(api_health_check::post__tuono_internal_api))"#
|
||||
));
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/health_check", get(api_health_check::get__tuono_internal_api))"#
|
||||
r#".route("/api/health_check", post(api_health_check::post_tuono_internal_api))"#
|
||||
));
|
||||
assert!(temp_main_rs_content
|
||||
.contains(r#".route("/api/health_check", get(api_health_check::get_tuono_internal_api))"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -129,19 +127,20 @@ fn it_successfully_create_catch_all_routes() {
|
||||
assert!(temp_main_rs_content.contains("mod dyn_catch_all_all_routes;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/*all_apis", post(api_dyn_catch_all_all_apis::post__tuono_internal_api))"#
|
||||
r#".route("/api/{*all_apis}", post(api_dyn_catch_all_all_apis::post_tuono_internal_api))"#
|
||||
));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/*all_routes", get(dyn_catch_all_all_routes::tuono__internal__route))"#
|
||||
r#".route("/{*all_routes}", get(dyn_catch_all_all_routes::tuono_internal_route))"#
|
||||
));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/*all_routes", get(dyn_catch_all_all_routes::tuono__internal__route))"#
|
||||
r#".route("/{*all_routes}", get(dyn_catch_all_all_routes::tuono_internal_route))"#
|
||||
));
|
||||
|
||||
assert!(temp_main_rs_content
|
||||
.contains(r#".route("/__tuono/data/*all_routes", get(dyn_catch_all_all_routes::tuono__internal__api))"#));
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/__tuono/data/{*all_routes}", get(dyn_catch_all_all_routes::tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -168,11 +167,12 @@ fn it_fails_without_installed_build_script() {
|
||||
.assert()
|
||||
.success();
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr("[CLI] Failed to read tuono.config.ts\n");
|
||||
.stderr("Failed to read config. Please run `npm install` to generate automatically.\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use assert_cmd::Command;
|
||||
use clap::crate_version;
|
||||
use serial_test::serial;
|
||||
|
||||
use std::fs;
|
||||
|
||||
mod utils;
|
||||
|
||||
use utils::mock_github_endpoint::GitHubServerMock;
|
||||
use utils::temp_tuono_project::TempTuonoProject;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn it_creates_a_new_project_and_replace_the_versions_in_the_manifest() {
|
||||
let GitHubServerMock { env_vars, .. } = GitHubServerMock::new().await;
|
||||
|
||||
let temp_project = TempTuonoProject::new();
|
||||
|
||||
let project_folder = "my-project";
|
||||
|
||||
let mut cmd = Command::cargo_bin("tuono").unwrap();
|
||||
|
||||
cmd.arg("new")
|
||||
.arg(project_folder)
|
||||
.envs(env_vars)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let main_rs_path = temp_project
|
||||
.path()
|
||||
.join(project_folder)
|
||||
.join("src")
|
||||
.join("main.rs");
|
||||
|
||||
let cargo_toml_path = temp_project.path().join(project_folder).join("Cargo.toml");
|
||||
let package_json_path = temp_project
|
||||
.path()
|
||||
.join(project_folder)
|
||||
.join("package.json");
|
||||
|
||||
let main_rs_content = fs::read_to_string(main_rs_path).unwrap();
|
||||
let cargo_toml_content = fs::read_to_string(cargo_toml_path).unwrap();
|
||||
let package_json_content = fs::read_to_string(package_json_path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
main_rs_content,
|
||||
"fn main() { println!(\"Hello, world!\"); }"
|
||||
);
|
||||
|
||||
let version = crate_version!();
|
||||
|
||||
let expected_cargo_toml_content =
|
||||
format!("[package] name = \"tuono-tutorial\" [dependencies] tuono_lib = \"{version}\"");
|
||||
|
||||
assert_eq!(cargo_toml_content, expected_cargo_toml_content);
|
||||
|
||||
let expected_package_json_content =
|
||||
format!("{{\"name\": \"tuono-app\", \"dependencies\": {{ \"tuono\": \"{version}\" }}}}");
|
||||
|
||||
assert_eq!(package_json_content, expected_package_json_content);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use clap::crate_version;
|
||||
use std::env;
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct GitHubServerMock {
|
||||
pub server: MockServer,
|
||||
pub env_vars: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl GitHubServerMock {
|
||||
pub async fn new() -> Self {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let tuono_version = crate_version!();
|
||||
|
||||
let sha = "1234567890abcdef";
|
||||
|
||||
let sha_response_template = ResponseTemplate::new(200).set_body_raw(
|
||||
format!("{{ \"object\": {{ \"sha\": \"{sha}\" }} }}"),
|
||||
"application/json",
|
||||
);
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(&format!(
|
||||
"repos/tuono-labs/tuono/git/ref/tags/v{}",
|
||||
tuono_version
|
||||
)))
|
||||
.respond_with(sha_response_template)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let tree_response_template = ResponseTemplate::new(200).set_body_raw(
|
||||
r#"{
|
||||
"tree": [
|
||||
{
|
||||
"path": "examples/tuono-app/src",
|
||||
"type": "tree"
|
||||
},
|
||||
{
|
||||
"path": "examples/tuono-app/src/main.rs",
|
||||
"type": "blob"
|
||||
},
|
||||
{
|
||||
"path": "examples/tuono-app/Cargo.toml",
|
||||
"type": "blob"
|
||||
},
|
||||
{
|
||||
"path": "examples/tuono-app/package.json",
|
||||
"type": "blob"
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
"application/json",
|
||||
);
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(&format!("repos/tuono-labs/tuono/git/trees/{}", sha)))
|
||||
.respond_with(tree_response_template)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let file_response_template =
|
||||
|content: &str| ResponseTemplate::new(200).set_body_raw(content, "text/plain");
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"tuono-labs/tuono/v{tuono_version}/examples/tuono-app/src/main.rs"
|
||||
)))
|
||||
.respond_with(file_response_template(
|
||||
"fn main() { println!(\"Hello, world!\"); }",
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"tuono-labs/tuono/v{tuono_version}/examples/tuono-app/Cargo.toml"
|
||||
)))
|
||||
.respond_with(file_response_template(
|
||||
"[package] name = \"tuono-tutorial\" [dependencies] tuono_lib = { path = \"../../crates/tuono_lib/\" }"
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"tuono-labs/tuono/v{tuono_version}/examples/tuono-app/package.json"
|
||||
)))
|
||||
.respond_with(file_response_template(
|
||||
r#"{"name": "tuono-app", "dependencies": { "tuono": "link:../../packages/tuono" }}"#,
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let env_vars = vec![("__INTERNAL_TUONO_TEST".to_string(), server.uri())];
|
||||
GitHubServerMock { server, env_vars }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod mock_github_endpoint;
|
||||
pub mod temp_tuono_project;
|
||||
@@ -11,6 +11,7 @@ pub struct TempTuonoProject {
|
||||
temp_dir: TempDir,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TempTuonoProject {
|
||||
pub fn new() -> Self {
|
||||
let project = TempTuonoProject::new_with_no_config();
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_internal"
|
||||
version = "0.17.8"
|
||||
version = "0.17.9"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "Superfast React fullstack framework"
|
||||
|
||||
@@ -43,7 +43,7 @@ impl TempTuonoProject {
|
||||
impl Drop for TempTuonoProject {
|
||||
fn drop(&mut self) {
|
||||
// Set back the current dir in the previous state
|
||||
env::set_current_dir(self.original_dir.to_owned())
|
||||
env::set_current_dir(&self.original_dir)
|
||||
.expect("Failed to restore the original directory.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.17.8"
|
||||
version = "0.17.9"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "Superfast React fullstack framework"
|
||||
@@ -18,8 +18,8 @@ include = [
|
||||
|
||||
[dependencies]
|
||||
ssr_rs = "0.8.1"
|
||||
axum = {version = "0.7.5", features = ["json", "ws"]}
|
||||
axum-extra = {version = "0.9.6", features = ["cookie"]}
|
||||
axum = {version = "0.8.1", features = ["json", "ws"]}
|
||||
axum-extra = {version = "0.10.0", features = ["cookie"]}
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
erased-serde = "0.4.5"
|
||||
@@ -32,12 +32,12 @@ either = "1.13.0"
|
||||
tower-http = {version = "0.6.0", features = ["fs"]}
|
||||
colored = "2.1.0"
|
||||
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.17.8"}
|
||||
tuono_internal = {path = "../tuono_internal", version = "0.17.8"}
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.17.9"}
|
||||
tuono_internal = {path = "../tuono_internal", version = "0.17.9"}
|
||||
# Match the same version used by axum
|
||||
tokio-tungstenite = "0.24.0"
|
||||
tokio-tungstenite = "0.26.0"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
||||
tungstenite = "0.24.0"
|
||||
tungstenite = "0.26.0"
|
||||
http = "1.1.0"
|
||||
pin-project = "1.1.7"
|
||||
tower = "0.5.1"
|
||||
|
||||
@@ -166,7 +166,7 @@ mod tests {
|
||||
|
||||
use crate::manifest::BundleInfo;
|
||||
|
||||
fn prepare_payload<'a>(uri: Option<&'a str>, mode: Mode) -> Payload<'a> {
|
||||
fn prepare_payload(uri: Option<&str>, mode: Mode) -> Payload {
|
||||
let mut manifest_mock = HashMap::new();
|
||||
manifest_mock.insert(
|
||||
"client-main".to_string(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use axum::http::{HeaderMap, Uri};
|
||||
@@ -51,4 +51,94 @@ impl Request {
|
||||
pub fn location(&self) -> Location {
|
||||
Location::from(self.uri.to_owned())
|
||||
}
|
||||
|
||||
pub fn body<'de, T: Deserialize<'de>>(&'de self) -> Result<T, BodyParseError> {
|
||||
if let Some(body) = self.headers.get("body") {
|
||||
if let Ok(body) = body.to_str() {
|
||||
let body = serde_json::from_str::<T>(body)?;
|
||||
return Ok(body);
|
||||
}
|
||||
return Err(BodyParseError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Failed to read body",
|
||||
)));
|
||||
}
|
||||
Err(BodyParseError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"No body found",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BodyParseError {
|
||||
Io(std::io::Error),
|
||||
Serde(serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for BodyParseError {
|
||||
fn from(err: serde_json::Error) -> BodyParseError {
|
||||
BodyParseError::Serde(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FakeBody {
|
||||
field1: bool,
|
||||
field2: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_correctly_parse_the_body() {
|
||||
let mut request = Request::new(
|
||||
Uri::from_static("http://localhost:3000"),
|
||||
HeaderMap::new(),
|
||||
HashMap::new(),
|
||||
);
|
||||
|
||||
request.headers.insert(
|
||||
"body",
|
||||
r#"{"field1": true, "field2": "hello"}"#.parse().unwrap(),
|
||||
);
|
||||
|
||||
let body: FakeBody = request.body().expect("Failed to parse body");
|
||||
|
||||
assert!(body.field1);
|
||||
assert_eq!(body.field2, "hello".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_trigger_an_error_when_no_body_is_found() {
|
||||
let request = Request::new(
|
||||
Uri::from_static("http://localhost:3000"),
|
||||
HeaderMap::new(),
|
||||
HashMap::new(),
|
||||
);
|
||||
|
||||
let body: Result<FakeBody, BodyParseError> = request.body();
|
||||
|
||||
assert!(body.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_trigger_an_error_when_body_is_invalid() {
|
||||
let mut request = Request::new(
|
||||
Uri::from_static("http://localhost:3000"),
|
||||
HeaderMap::new(),
|
||||
HashMap::new(),
|
||||
);
|
||||
|
||||
request
|
||||
.headers
|
||||
.insert("body", r#"{"field1": true"#.parse().unwrap());
|
||||
|
||||
let body: Result<FakeBody, BodyParseError> = request.body();
|
||||
|
||||
assert!(body.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ impl Server {
|
||||
.to_owned()
|
||||
.layer(LoggerLayer::new())
|
||||
.route("/vite-server/", get(vite_websocket_proxy))
|
||||
.route("/vite-server/*path", get(vite_reverse_proxy))
|
||||
.route("/vite-server/{*path}", get(vite_reverse_proxy))
|
||||
.fallback_service(
|
||||
ServeDir::new(DEV_PUBLIC_DIR)
|
||||
.fallback(get(catch_all).layer(LoggerLayer::new())),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::config::GLOBAL_CONFIG;
|
||||
use axum::extract::ws::{self, WebSocket, WebSocketUpgrade};
|
||||
use axum::extract::ws::{self, Utf8Bytes as AxumUtf8Bytes, WebSocket, WebSocketUpgrade};
|
||||
use axum::response::IntoResponse;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use tokio_tungstenite::connect_async;
|
||||
@@ -64,7 +64,7 @@ async fn handle_socket(mut tuono_socket: WebSocket) {
|
||||
while let Some(msg) = tuono_receiver.next().await {
|
||||
if let Ok(msg) = msg {
|
||||
let msg_to_vite = match msg.clone() {
|
||||
ws::Message::Text(str) => Message::Text(str),
|
||||
ws::Message::Text(str) => Message::Text(str.to_string().into()),
|
||||
ws::Message::Pong(payload) => Message::Pong(payload),
|
||||
ws::Message::Ping(payload) => Message::Ping(payload),
|
||||
ws::Message::Binary(payload) => Message::Binary(payload),
|
||||
@@ -91,7 +91,7 @@ async fn handle_socket(mut tuono_socket: WebSocket) {
|
||||
tokio::spawn(async move {
|
||||
while let Some(Ok(msg)) = vite_receiver.next().await {
|
||||
let msg_to_browser = match msg {
|
||||
Message::Text(str) => ws::Message::Text(str),
|
||||
Message::Text(str) => ws::Message::Text(AxumUtf8Bytes::from(str.to_string())),
|
||||
Message::Ping(payload) => ws::Message::Ping(payload),
|
||||
Message::Pong(payload) => ws::Message::Pong(payload),
|
||||
Message::Binary(payload) => ws::Message::Binary(payload),
|
||||
@@ -100,7 +100,7 @@ async fn handle_socket(mut tuono_socket: WebSocket) {
|
||||
Message::Close(_) => ws::Message::Close(None),
|
||||
_ => {
|
||||
eprintln!("Unexpected message from the vite WebSocket to the browser: {msg:?}");
|
||||
ws::Message::Text("Unhandled".to_string())
|
||||
ws::Message::Text("Unhandled".to_string().into())
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mod utils;
|
||||
use crate::utils::utils::MockTuonoServer;
|
||||
use crate::utils::mock_server::MockTuonoServer;
|
||||
use serial_test::serial;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -15,7 +15,7 @@ async fn api_endpoint_work() {
|
||||
let server_url = format!("http://{}:{}", &app.address, &app.port);
|
||||
|
||||
let response = client
|
||||
.get(&format!("{server_url}/health_check"))
|
||||
.get(format!("{server_url}/health_check"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request.");
|
||||
@@ -38,7 +38,7 @@ async fn not_found_route() {
|
||||
let server_url = format!("http://{}:{}", &app.address, &app.port);
|
||||
|
||||
let response = client
|
||||
.get(&format!("{server_url}/not-found"))
|
||||
.get(format!("{server_url}/not-found"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request.");
|
||||
@@ -64,7 +64,7 @@ async fn index_html_route() {
|
||||
let server_url = format!("http://{}:{}", &app.address, &app.port);
|
||||
|
||||
let response = client
|
||||
.get(&format!("{server_url}/"))
|
||||
.get(format!("{server_url}/"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request.");
|
||||
@@ -91,7 +91,7 @@ async fn api_route_route() {
|
||||
let server_url = format!("http://{}:{}", &app.address, &app.port);
|
||||
|
||||
let response = client
|
||||
.get(&format!("{server_url}/tuono/data"))
|
||||
.get(format!("{server_url}/tuono/data"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request.");
|
||||
@@ -103,3 +103,47 @@ async fn api_route_route() {
|
||||
"{\"data\":\"{}\",\"info\":{\"redirect_destination\":null}}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn it_reads_the_catch_all_path_parameter() {
|
||||
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 response = client
|
||||
.get(format!("{server_url}/catch_all/url_parameter"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request.");
|
||||
|
||||
assert!(response.status().is_success());
|
||||
assert_eq!(response.text().await.unwrap(), "url_parameter");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn it_reads_the_path_parameter() {
|
||||
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 response = client
|
||||
.get(format!("{server_url}/dynamic/url_parameter"))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request.");
|
||||
|
||||
assert!(response.status().is_success());
|
||||
assert_eq!(response.text().await.unwrap(), "url_parameter");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use tuono_lib::Request;
|
||||
|
||||
#[tuono_lib::api(GET)]
|
||||
async fn read_catch_all_parameter(req: Request) -> String {
|
||||
let param = req
|
||||
.params
|
||||
.get("catch_all")
|
||||
.expect("Failed to get the catch_all param");
|
||||
|
||||
param.to_string()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use tuono_lib::Request;
|
||||
|
||||
#[tuono_lib::api(GET)]
|
||||
async fn read_dynamic_parameter(req: Request) -> String {
|
||||
let param = req
|
||||
.params
|
||||
.get("parameter")
|
||||
.expect("Failed to get the catch_all param");
|
||||
|
||||
param.to_string()
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
use fs_extra::dir::create_all;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
use tuono_lib::axum::routing::get;
|
||||
use tuono_lib::{axum::Router, tuono_internal_init_v8_platform, Mode, Server};
|
||||
|
||||
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::health_check::get_tuono_internal_api as health_check;
|
||||
use crate::utils::route as html_route;
|
||||
use crate::utils::route::tuono_internal_api as route_api;
|
||||
|
||||
use std::sync::Once;
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
fn init_v8() {
|
||||
INIT.call_once(|| {
|
||||
tuono_internal_init_v8_platform();
|
||||
})
|
||||
}
|
||||
|
||||
fn add_file_with_content<'a>(path: &'a str, content: &'a str) {
|
||||
let path = PathBuf::from(path);
|
||||
create_all(
|
||||
path.parent().expect("File path does not have any parent"),
|
||||
false,
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Failed to create parent file directories: {}",
|
||||
path.display()
|
||||
)
|
||||
});
|
||||
|
||||
let mut file = File::create(path).expect("Failed to create the file");
|
||||
file.write_all(content.as_bytes())
|
||||
.expect("Failed to write into the file");
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MockTuonoServer {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
original_dir: PathBuf,
|
||||
#[allow(dead_code)]
|
||||
// Required for dropping the temp_dir when this struct drops
|
||||
temp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl MockTuonoServer {
|
||||
pub async fn spawn() -> Self {
|
||||
init_v8();
|
||||
let original_dir = env::current_dir().expect("Failed to read current_dir");
|
||||
let temp_dir = tempdir().expect("Failed to create temp_dir");
|
||||
|
||||
let react_prod_build = fs::read_to_string("./tests/assets/fake_react_build.js")
|
||||
.expect("Failed to read fake_react_build.js");
|
||||
|
||||
env::set_current_dir(temp_dir.path()).expect("Failed to change current dir into temp_dir");
|
||||
|
||||
add_file_with_content(
|
||||
"./.tuono/config/config.json",
|
||||
r#"{"server": {"host": "127.0.0.1", "port": 0}}"#,
|
||||
);
|
||||
|
||||
add_file_with_content("./out/server/prod-server.js", react_prod_build.as_str());
|
||||
|
||||
add_file_with_content(
|
||||
"./out/client/.vite/manifest.json",
|
||||
r#"{"client-main.tsx": { "file": "assets/index.js", "name": "index", "src": "index.tsx", "isEntry": true,"dynamicImports": [],"css": []}}"#,
|
||||
);
|
||||
|
||||
let router = Router::new()
|
||||
.route("/", get(html_route::tuono_internal_route))
|
||||
.route("/tuono/data", get(html_route::tuono_internal_api))
|
||||
.route("/health_check", get(health_check))
|
||||
.route("/route-api", get(route_api))
|
||||
.route("/catch_all/{*catch_all}", get(catch_all))
|
||||
.route("/dynamic/{parameter}", get(dynamic_parameter));
|
||||
|
||||
let server = Server::init(router, Mode::Prod).await;
|
||||
|
||||
let socket = server
|
||||
.listener
|
||||
.local_addr()
|
||||
.expect("Failed to extract test server socket");
|
||||
|
||||
_ = tokio::spawn(server.start());
|
||||
|
||||
MockTuonoServer {
|
||||
address: socket.ip().to_string(),
|
||||
port: socket.port(),
|
||||
original_dir,
|
||||
temp_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MockTuonoServer {
|
||||
fn drop(&mut self) {
|
||||
// Set back the current dir in the previous state
|
||||
env::set_current_dir(&self.original_dir)
|
||||
.expect("Failed to restore the original directory.");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod catch_all;
|
||||
pub mod dynamic_parameter;
|
||||
pub mod health_check;
|
||||
pub mod mock_server;
|
||||
pub mod route;
|
||||
pub mod utils;
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
use fs_extra::dir::create_all;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
use tuono_lib::axum::routing::get;
|
||||
use tuono_lib::{axum::Router, tuono_internal_init_v8_platform, Mode, Server};
|
||||
|
||||
use crate::utils::health_check::get__tuono_internal_api as health_check;
|
||||
use crate::utils::route as html_route;
|
||||
use crate::utils::route::tuono__internal__api as route_api;
|
||||
|
||||
use std::sync::Once;
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
fn init_v8() {
|
||||
INIT.call_once(|| {
|
||||
tuono_internal_init_v8_platform();
|
||||
})
|
||||
}
|
||||
|
||||
fn add_file_with_content<'a>(path: &'a str, content: &'a str) {
|
||||
let path = PathBuf::from(path);
|
||||
create_all(
|
||||
path.parent().expect("File path does not have any parent"),
|
||||
false,
|
||||
)
|
||||
.expect(
|
||||
format!(
|
||||
"Failed to create parent file directories: {}",
|
||||
path.display()
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let mut file = File::create(path).expect("Failed to create the file");
|
||||
file.write_all(content.as_bytes())
|
||||
.expect("Failed to write into the file");
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MockTuonoServer {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
original_dir: PathBuf,
|
||||
#[allow(dead_code)]
|
||||
// Required for dropping the temp_dir when this struct drops
|
||||
temp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl MockTuonoServer {
|
||||
pub async fn spawn() -> Self {
|
||||
init_v8();
|
||||
let original_dir = env::current_dir().expect("Failed to read current_dir");
|
||||
let temp_dir = tempdir().expect("Failed to create temp_dir");
|
||||
|
||||
let react_prod_build = fs::read_to_string("./tests/assets/fake_react_build.js")
|
||||
.expect("Failed to read fake_react_build.js");
|
||||
|
||||
env::set_current_dir(temp_dir.path()).expect("Failed to change current dir into temp_dir");
|
||||
|
||||
add_file_with_content(
|
||||
"./.tuono/config/config.json",
|
||||
r#"{"server": {"host": "127.0.0.1", "port": 0}}"#,
|
||||
);
|
||||
|
||||
add_file_with_content("./out/server/prod-server.js", react_prod_build.as_str());
|
||||
|
||||
add_file_with_content(
|
||||
"./out/client/.vite/manifest.json",
|
||||
r#"{"client-main.tsx": { "file": "assets/index.js", "name": "index", "src": "index.tsx", "isEntry": true,"dynamicImports": [],"css": []}}"#,
|
||||
);
|
||||
|
||||
let router = Router::new()
|
||||
.route("/", get(html_route::tuono__internal__route))
|
||||
.route("/tuono/data", get(html_route::tuono__internal__api))
|
||||
.route("/health_check", get(health_check))
|
||||
.route("/route-api", get(route_api));
|
||||
|
||||
let server = Server::init(router, Mode::Prod).await;
|
||||
|
||||
let socket = server
|
||||
.listener
|
||||
.local_addr()
|
||||
.expect("Failed to extract test server socket");
|
||||
|
||||
let _ = tokio::spawn(server.start());
|
||||
|
||||
MockTuonoServer {
|
||||
address: socket.ip().to_string(),
|
||||
port: socket.port(),
|
||||
original_dir,
|
||||
temp_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MockTuonoServer {
|
||||
fn drop(&mut self) {
|
||||
// Set back the current dir in the previous state
|
||||
env::set_current_dir(self.original_dir.to_owned())
|
||||
.expect("Failed to restore the original directory.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib_macros"
|
||||
version = "0.17.8"
|
||||
version = "0.17.9"
|
||||
edition = "2021"
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn api_core(attrs: TokenStream, item: TokenStream) -> TokenStream {
|
||||
.to_lowercase();
|
||||
|
||||
let api_fn_name = Ident::new(
|
||||
&format!("{}__tuono_internal_api", http_method),
|
||||
&format!("{}_tuono_internal_api", http_method),
|
||||
Span::call_site().into(),
|
||||
);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
|
||||
#item
|
||||
|
||||
pub async fn tuono__internal__route(
|
||||
pub async fn tuono_internal_route(
|
||||
#axum_arguments
|
||||
) -> impl tuono_lib::axum::response::IntoResponse {
|
||||
|
||||
@@ -59,7 +59,7 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
#fn_name(req.clone(), #argument_names).await.render_to_string(req)
|
||||
}
|
||||
|
||||
pub async fn tuono__internal__api(
|
||||
pub async fn tuono_internal_api(
|
||||
#axum_arguments
|
||||
) -> impl tuono_lib::axum::response::IntoResponse {
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "vite-config",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"format:fix": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"oxc-transform": "0.52.0",
|
||||
"rollup-plugin-preserve-directives": "0.4.0",
|
||||
"unplugin-isolated-decl": "0.11.2",
|
||||
"vite": "6.1.1",
|
||||
"vite-plugin-externalize-deps": "0.9.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { UserConfig } from 'vite'
|
||||
import { preserveDirectives } from 'rollup-plugin-preserve-directives'
|
||||
import { externalizeDeps } from 'vite-plugin-externalize-deps'
|
||||
import UnpluginIsolatedDecl from 'unplugin-isolated-decl/vite'
|
||||
|
||||
interface Options {
|
||||
/** Entry file, e.g. `./src/index.ts` */
|
||||
entry: string | Array<string>
|
||||
}
|
||||
|
||||
export function defineViteConfig({ entry }: Options): UserConfig {
|
||||
const outDir = 'dist'
|
||||
|
||||
return {
|
||||
build: {
|
||||
outDir,
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry,
|
||||
formats: ['es'],
|
||||
fileName: 'esm/[name]',
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
preserveModules: true,
|
||||
entryFileNames: 'esm/[name].js',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
externalizeDeps(),
|
||||
preserveDirectives(),
|
||||
UnpluginIsolatedDecl({ transformer: 'oxc' }),
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
// Modules
|
||||
"moduleResolution": "node16",
|
||||
"module": "Node16",
|
||||
|
||||
// Emit
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -6,6 +6,9 @@ import eslintPluginReact from 'eslint-plugin-react'
|
||||
// @ts-expect-error no types are available for this plugin
|
||||
import eslintPluginReactHooks from 'eslint-plugin-react-hooks'
|
||||
|
||||
const REACT_FILES_MATCH =
|
||||
'packages/{tuono,tuono-router}/**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'
|
||||
|
||||
/** @type import('typescript-eslint').ConfigArray */
|
||||
const tuonoEslintConfig = tseslint.config(
|
||||
{
|
||||
@@ -44,12 +47,17 @@ const tuonoEslintConfig = tseslint.config(
|
||||
// eslint-disable-next-line import/no-named-as-default-member
|
||||
tseslint.configs.strictTypeChecked,
|
||||
|
||||
// @ts-expect-error flat is optional but always defined on runtime
|
||||
eslintPluginReact.configs.flat.recommended,
|
||||
eslintPluginReact.configs.flat['jsx-runtime'],
|
||||
|
||||
// #region react
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
|
||||
files: [REACT_FILES_MATCH],
|
||||
...eslintPluginReact.configs.flat.recommended,
|
||||
},
|
||||
{
|
||||
files: [REACT_FILES_MATCH],
|
||||
...eslintPluginReact.configs.flat['jsx-runtime'],
|
||||
},
|
||||
{
|
||||
files: [REACT_FILES_MATCH],
|
||||
plugins: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
'react-hooks': eslintPluginReactHooks,
|
||||
@@ -59,6 +67,7 @@ const tuonoEslintConfig = tseslint.config(
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
},
|
||||
},
|
||||
// #endregion react
|
||||
|
||||
{
|
||||
languageOptions: {
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
// Typechecking
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Modules
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
// Interop Constraints
|
||||
"isolatedModules": true,
|
||||
|
||||
// Language and Environment
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"useDefineForClassFields": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
// Emit
|
||||
"noEmit": true,
|
||||
|
||||
// Completeness
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src", "tuono.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Language and Environment
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"useDefineForClassFields": true,
|
||||
|
||||
// Modules
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
|
||||
// Emit
|
||||
"noEmit": true,
|
||||
|
||||
// Interop Constraints
|
||||
"isolatedModules": true,
|
||||
|
||||
// Typechecking
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Modules
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
// Interop Constraints
|
||||
"isolatedModules": true,
|
||||
|
||||
// Language and Environment
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"useDefineForClassFields": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
// Emit
|
||||
"noEmit": true,
|
||||
|
||||
// Completeness
|
||||
"skipLibCheck": true
|
||||
},
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
// Typechecking
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Modules
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
// Interop Constraints
|
||||
"isolatedModules": true,
|
||||
|
||||
// Language and Environment
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"useDefineForClassFields": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
// Emit
|
||||
"noEmit": true,
|
||||
|
||||
// Completeness
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src", "tuono.config.ts"]
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"description": "Basic tuono application with tailwind",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0-beta.9",
|
||||
"@tailwindcss/vite": "^4.0.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwindcss": "^4.0.0-beta.9",
|
||||
"tailwindcss": "^4.0.6",
|
||||
"tuono": "link:../../packages/tuono"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
// Typechecking
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Modules
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
// Interop Constraints
|
||||
"isolatedModules": true,
|
||||
|
||||
// Language and Environment
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"useDefineForClassFields": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
// Emit
|
||||
"noEmit": true,
|
||||
|
||||
// Completeness
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src", "tuono.config.ts"]
|
||||
}
|
||||
|
||||
+15
-15
@@ -4,32 +4,32 @@
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0",
|
||||
"scripts": {
|
||||
"dev": "turbo dev --filter=./packages/*",
|
||||
"build": "turbo build --filter=./packages/*",
|
||||
"lint": "turbo lint --filter=./packages/*",
|
||||
"format": "turbo format --filter=./packages/*",
|
||||
"format:check": "turbo format:check --filter=./packages/*",
|
||||
"types": "turbo types --filter=./packages/*",
|
||||
"test": "turbo test --filter=./packages/*",
|
||||
"test:watch": "turbo test:watch --filter=./packages/*",
|
||||
"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": "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",
|
||||
"repo:root:format:check": "prettier . !./assets/** !./crates !./examples !./packages/** --check",
|
||||
"repo:root:format": "prettier . !./assets/** !./crates !./examples !./packages/** --write",
|
||||
"check-all": "turbo build lint format:check types --filter=!./examples"
|
||||
"repo:root:format": "prettier . !./assets/** !./crates !./examples !./devtools/** !./packages/** --check",
|
||||
"repo:root:format:fix": "prettier . !./assets/** !./crates !./examples !./devtools/** !./packages/** --write",
|
||||
"check-all": "turbo build lint format typecheck --filter=!./examples"
|
||||
},
|
||||
"author": "Valerio Ageno",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.20.0",
|
||||
"@types/node": "22.13.1",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@types/node": "22.13.5",
|
||||
"eslint": "9.20.0",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"eslint-import-resolver-typescript": "3.7.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
"eslint-plugin-react-hooks": "5.1.0",
|
||||
"prettier": "3.5.0",
|
||||
"turbo": "2.4.0",
|
||||
"prettier": "3.5.2",
|
||||
"turbo": "2.4.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.24.0"
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "tuono-fs-router-vite-plugin",
|
||||
"version": "0.17.8",
|
||||
"version": "0.17.9",
|
||||
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
|
||||
"homepage": "https://tuono.dev",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"format:check": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"types": "tsc --noEmit",
|
||||
"format": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"format:fix": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:watch": "vitest",
|
||||
"test": "vitest run"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"main": "dist/esm/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -32,14 +32,8 @@
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/index.d.cts",
|
||||
"default": "./dist/cjs/index.cjs"
|
||||
}
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
@@ -47,11 +41,11 @@
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/types": "^7.24.0",
|
||||
"prettier": "^3.2.4",
|
||||
"vite": "^5.4.14"
|
||||
"vite": "^6.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/config": "0.7.13",
|
||||
"@types/babel__core": "7.20.5",
|
||||
"vitest": "2.1.9"
|
||||
"vite-config": "workspace:*",
|
||||
"vitest": "3.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,9 @@ async function getRouteNodes(
|
||||
return { routeNodes, rustHandlersNodes }
|
||||
}
|
||||
|
||||
export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
export async function routeGenerator(
|
||||
config: Config = defaultConfig,
|
||||
): Promise<void> {
|
||||
if (!isFirst) {
|
||||
isFirst = true
|
||||
} else if (skipMessage) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { normalize } from 'path'
|
||||
import { normalize } from 'node:path'
|
||||
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { defineConfig, mergeConfig } from 'vitest/config'
|
||||
import { tanstackBuildConfig } from '@tanstack/config/build'
|
||||
import { defineViteConfig } from 'vite-config'
|
||||
|
||||
const config = defineConfig({})
|
||||
|
||||
export default mergeConfig(
|
||||
config,
|
||||
tanstackBuildConfig({
|
||||
entry: './src/index.ts',
|
||||
srcDir: './src',
|
||||
}),
|
||||
)
|
||||
export default defineViteConfig({
|
||||
entry: './src/index.ts',
|
||||
})
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "tuono-router",
|
||||
"version": "0.17.8",
|
||||
"version": "0.17.9",
|
||||
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
|
||||
"homepage": "https://tuono.dev",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"format:check": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"types": "tsc --noEmit",
|
||||
"format": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"format:fix": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:watch": "vitest",
|
||||
"test": "vitest run"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"main": "dist/esm/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -32,14 +32,8 @@
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/index.d.cts",
|
||||
"default": "./dist/cjs/index.cjs"
|
||||
}
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
@@ -50,13 +44,13 @@
|
||||
"react-intersection-observer": "^9.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/config": "0.7.13",
|
||||
"@testing-library/react": "16.2.0",
|
||||
"@types/react": "19.0.8",
|
||||
"@vitejs/plugin-react-swc": "3.7.2",
|
||||
"happy-dom": "16.8.1",
|
||||
"@vitejs/plugin-react-swc": "3.8.0",
|
||||
"happy-dom": "17.1.4",
|
||||
"react": "19.0.0",
|
||||
"vite": "5.4.14",
|
||||
"vitest": "2.1.9"
|
||||
"vite": "6.1.1",
|
||||
"vite-config": "workspace:*",
|
||||
"vitest": "3.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ import { render, fireEvent, screen } from '@testing-library/react'
|
||||
import Link from './Link'
|
||||
|
||||
const pushMock = vi.fn()
|
||||
const replaceMock = vi.fn()
|
||||
const preloadMock = vi.fn()
|
||||
|
||||
vi.mock('../hooks/useRouter', () => ({
|
||||
useRouter: (): { push: typeof pushMock } => ({ push: pushMock }),
|
||||
useRouter: (): { push: typeof pushMock; replace: typeof replaceMock } => ({
|
||||
push: pushMock,
|
||||
replace: replaceMock,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/useRoute', () => ({
|
||||
@@ -51,6 +55,19 @@ describe('Link Component', () => {
|
||||
expect(pushMock).toHaveBeenCalledWith('/test', { scroll: true })
|
||||
})
|
||||
|
||||
it('calls router.replace on click when the replace prop is true', () => {
|
||||
render(
|
||||
<Link href="/test" replace>
|
||||
Test Link
|
||||
</Link>,
|
||||
)
|
||||
const link = screen.getByRole('link')
|
||||
|
||||
fireEvent.click(link)
|
||||
expect(replaceMock).toHaveBeenCalledWith('/test', { scroll: true })
|
||||
expect(pushMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not navigate if href starts with "#"', () => {
|
||||
render(<Link href="#section">Anchor Link</Link>)
|
||||
const link = screen.getByRole('link')
|
||||
|
||||
@@ -16,6 +16,12 @@ interface TuonoLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
* @default true
|
||||
*/
|
||||
scroll?: boolean
|
||||
|
||||
/**
|
||||
* If "true" the history entry will be replaced instead of pushed.
|
||||
* @default false
|
||||
*/
|
||||
replace?: boolean
|
||||
}
|
||||
|
||||
function isEventModifierKeyActiveAndTargetDifferentFromSelf(
|
||||
@@ -39,6 +45,7 @@ export default function Link(
|
||||
scroll = true,
|
||||
children,
|
||||
href,
|
||||
replace,
|
||||
onClick,
|
||||
...rest
|
||||
} = componentProps
|
||||
@@ -68,7 +75,9 @@ export default function Link(
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
router.push(href || '', { scroll })
|
||||
const method = replace ? 'replace' : 'push'
|
||||
|
||||
router[method](href || '', { scroll })
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,10 @@ import React from 'react'
|
||||
|
||||
import { useRouterContext } from '../components/RouterContext'
|
||||
|
||||
interface PushOptions {
|
||||
type NavigationType = 'pushState' | 'replaceState'
|
||||
type NavigationFn = (path: string, opts?: NavigationOptions) => void
|
||||
|
||||
interface NavigationOptions {
|
||||
/**
|
||||
* If "false" the scroll offset will be kept across page navigation. Default "true"
|
||||
*/
|
||||
@@ -13,7 +16,13 @@ interface UseRouterResult {
|
||||
/**
|
||||
* Redirects to the path passed as argument updating the browser history.
|
||||
*/
|
||||
push: (path: string, opt?: PushOptions) => void
|
||||
push: NavigationFn
|
||||
|
||||
/**
|
||||
* Redirects to the path passed as argument replacing the current history
|
||||
* entry.
|
||||
*/
|
||||
replace: NavigationFn
|
||||
|
||||
/**
|
||||
* This object contains all the query params of the current route
|
||||
@@ -29,9 +38,9 @@ interface UseRouterResult {
|
||||
export const useRouter = (): UseRouterResult => {
|
||||
const { location, updateLocation } = useRouterContext()
|
||||
|
||||
const push = React.useCallback(
|
||||
(path: string, opt?: PushOptions): void => {
|
||||
const { scroll = true } = opt || {}
|
||||
const navigate = React.useCallback(
|
||||
(type: NavigationType, path: string, opts?: NavigationOptions): void => {
|
||||
const { scroll = true } = opts || {}
|
||||
const url = new URL(path, window.location.origin)
|
||||
|
||||
updateLocation({
|
||||
@@ -41,7 +50,8 @@ export const useRouter = (): UseRouterResult => {
|
||||
searchStr: url.search,
|
||||
hash: url.hash,
|
||||
})
|
||||
history.pushState(path, '', path)
|
||||
|
||||
history[type](path, '', path)
|
||||
|
||||
if (scroll) {
|
||||
window.scroll(0, 0)
|
||||
@@ -50,8 +60,23 @@ export const useRouter = (): UseRouterResult => {
|
||||
[updateLocation],
|
||||
)
|
||||
|
||||
const push = React.useCallback(
|
||||
(path: string, opts?: NavigationOptions): void => {
|
||||
navigate('pushState', path, opts)
|
||||
},
|
||||
[navigate],
|
||||
)
|
||||
|
||||
const replace = React.useCallback(
|
||||
(path: string, opts?: NavigationOptions): void => {
|
||||
navigate('replaceState', path, opts)
|
||||
},
|
||||
[navigate],
|
||||
)
|
||||
|
||||
return {
|
||||
push,
|
||||
replace,
|
||||
query: location.search,
|
||||
pathname: location.pathname,
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class Router {
|
||||
basePath = '/'
|
||||
routeTree?: RouteTree
|
||||
|
||||
isServer = typeof document === 'undefined'
|
||||
isServer: boolean = typeof document === 'undefined'
|
||||
|
||||
routesById: Record<string, Route> = {}
|
||||
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
/// <reference types="vitest" />
|
||||
/// <reference types="vite/client" />
|
||||
import { defineConfig, mergeConfig } from 'vitest/config'
|
||||
import { tanstackBuildConfig } from '@tanstack/config/build'
|
||||
import { defineViteConfig } from 'vite-config'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
|
||||
const config = defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
name: 'tuono-router',
|
||||
environment: 'happy-dom',
|
||||
globals: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default mergeConfig(
|
||||
config,
|
||||
tanstackBuildConfig({
|
||||
defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
name: 'tuono-router',
|
||||
environment: 'happy-dom',
|
||||
globals: true,
|
||||
},
|
||||
}),
|
||||
defineViteConfig({
|
||||
entry: './src/index.ts',
|
||||
srcDir: './src',
|
||||
}),
|
||||
)
|
||||
|
||||
+20
-50
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"version": "0.17.8",
|
||||
"version": "0.17.9",
|
||||
"description": "Superfast React fullstack framework",
|
||||
"homepage": "https://tuono.dev",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"format:check": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"types": "tsc --noEmit",
|
||||
"format": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"format:fix": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:watch": "vitest",
|
||||
"test": "vitest run"
|
||||
},
|
||||
@@ -20,58 +20,28 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"main": "dist/esm/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"exports": {
|
||||
"./build": {
|
||||
"import": {
|
||||
"types": "./dist/esm/build/index.d.ts",
|
||||
"default": "./dist/esm/build/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/build/index.d.ts",
|
||||
"default": "./dist/cjs/build/index.js"
|
||||
}
|
||||
"types": "./dist/esm/build/index.d.ts",
|
||||
"default": "./dist/esm/build/index.js"
|
||||
},
|
||||
"./config": {
|
||||
"import": {
|
||||
"types": "./dist/esm/config/index.d.ts",
|
||||
"default": "./dist/esm/config/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/config/index.d.ts",
|
||||
"default": "./dist/cjs/config/index.js"
|
||||
}
|
||||
"types": "./dist/esm/config/index.d.ts",
|
||||
"default": "./dist/esm/config/index.js"
|
||||
},
|
||||
"./ssr": {
|
||||
"import": {
|
||||
"types": "./dist/esm/ssr/index.d.ts",
|
||||
"default": "./dist/esm/ssr/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/ssr/index.d.ts",
|
||||
"default": "./dist/cjs/ssr/index.js"
|
||||
}
|
||||
"types": "./dist/esm/ssr/index.d.ts",
|
||||
"default": "./dist/esm/ssr/index.js"
|
||||
},
|
||||
"./hydration": {
|
||||
"import": {
|
||||
"types": "./dist/esm/hydration/index.d.ts",
|
||||
"default": "./dist/esm/hydration/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/ssr/index.d.ts",
|
||||
"default": "./dist/cjs/ssr/index.js"
|
||||
}
|
||||
"types": "./dist/esm/hydration/index.d.ts",
|
||||
"default": "./dist/esm/hydration/index.js"
|
||||
},
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/index.d.cts",
|
||||
"default": "./dist/cjs/index.cjs"
|
||||
}
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
@@ -96,24 +66,24 @@
|
||||
"@babel/plugin-syntax-jsx": "^7.24.1",
|
||||
"@babel/plugin-syntax-typescript": "^7.24.1",
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"tuono-fs-router-vite-plugin": "workspace:*",
|
||||
"tuono-router": "workspace:*",
|
||||
"url-search-params-polyfill": "^8.2.5",
|
||||
"vite": "^5.4.14",
|
||||
"vite": "^6.1.1",
|
||||
"web-streams-polyfill": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/config": "0.7.13",
|
||||
"@types/babel__core": "7.20.5",
|
||||
"@types/babel__traverse": "7.20.6",
|
||||
"@types/node": "22.13.1",
|
||||
"@types/node": "22.13.5",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"vitest": "2.1.9"
|
||||
"vite-config": "workspace:*",
|
||||
"vitest": "3.0.7"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from 'fs/promises'
|
||||
|
||||
import { describe, expect, it, vitest } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vitest } from 'vitest'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
|
||||
import { createJsonConfig } from './create-json-config'
|
||||
@@ -8,6 +8,10 @@ import { createJsonConfig } from './create-json-config'
|
||||
const writeFileSpy = vitest.spyOn(fs, 'writeFile').mockResolvedValue(void 0)
|
||||
|
||||
describe('createJsonConfig', () => {
|
||||
beforeEach(() => {
|
||||
writeFileSpy.mockClear()
|
||||
})
|
||||
|
||||
const sampleConfig = { server: { host: 'h', port: 1 } }
|
||||
|
||||
it('should process config with only server property', async () => {
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
/// <reference types="vitest" />
|
||||
/// <reference types="vite/client" />
|
||||
import { defineConfig, mergeConfig } from 'vitest/config'
|
||||
import { tanstackBuildConfig } from '@tanstack/config/build'
|
||||
import { defineViteConfig } from 'vite-config'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
|
||||
const config = defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
|
||||
export default mergeConfig(
|
||||
config,
|
||||
tanstackBuildConfig({
|
||||
defineConfig({
|
||||
plugins: [react()],
|
||||
}),
|
||||
defineViteConfig({
|
||||
entry: [
|
||||
'./src/index.ts',
|
||||
'./src/build/index.ts',
|
||||
@@ -18,6 +16,5 @@ export default mergeConfig(
|
||||
'./src/ssr/index.ts',
|
||||
'./src/hydration/index.tsx',
|
||||
],
|
||||
srcDir: './src',
|
||||
}),
|
||||
)
|
||||
|
||||
Generated
+770
-1546
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
packages:
|
||||
- 'devtools/*'
|
||||
- 'packages/*'
|
||||
- 'examples/*'
|
||||
- 'e2e/fixtures/*'
|
||||
|
||||
+5
-3
@@ -6,12 +6,14 @@
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"format": {},
|
||||
"format:check": {},
|
||||
"types": {
|
||||
"format:fix": {},
|
||||
"typecheck": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputLogs": "new-only"
|
||||
},
|
||||
"test": {},
|
||||
"test": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"test:watch": {},
|
||||
"build": {
|
||||
"outputs": ["dist/**"],
|
||||
|
||||
Reference in New Issue
Block a user