Compare commits

...

52 Commits

Author SHA1 Message Date
Valerio Ageno 7d65ede581 feat: update version to v0.1.2 2024-06-12 13:23:08 +02:00
Valerio Ageno 5b11f68fbb feat: pass headers to request 2024-06-12 13:20:44 +02:00
Valerio Ageno 0d33e6887b feat: update pnpm and install locally turbo 2024-06-09 16:42:13 +02:00
Valerio Ageno 5bb620a590 Update tutorial.md 2024-06-08 12:50:36 +02:00
Valerio Ageno 469386388b feat: update version to v0.1.1 2024-06-08 12:45:36 +02:00
Valerio Ageno 8728499f90 feat: refined reload logs 2024-06-08 12:42:58 +02:00
Valerio Ageno 2e82dde2c3 feat: refine development command logs 2024-06-08 12:33:34 +02:00
Valerio Ageno 253dd44d0d feat: update version to v0.1.0 2024-06-07 21:03:54 +02:00
Valerio Ageno 821b38e269 feat: set listener on popstate to allow back and forward browser buttons 2024-06-07 21:01:40 +02:00
Valerio Ageno 753bd21c14 Update README.md 2024-06-07 12:22:20 +02:00
Valerio Ageno 10757d4ef4 doc: add tutorial mention in tutorial example/ 2024-06-06 20:57:31 +02:00
Valerio Ageno d436637086 doc: add tutorial mention on the README 2024-06-06 20:54:09 +02:00
Valerio Ageno 0b5679ee07 doc: create tutorial README 2024-06-06 20:50:58 +02:00
Valerio Ageno 97b6cdfaaa doc: add README to examples/ root 2024-06-06 19:43:49 +02:00
Valerio Ageno b10d01cae0 doc: add tutorial example 2024-06-06 19:41:21 +02:00
Valerio Ageno 2262264cae feat: update version to v0.0.12 2024-06-06 19:19:31 +02:00
Valerio Ageno bedd23eed8 feat(tuono new): update replaced key for package.json 2024-06-06 19:18:06 +02:00
Valerio Ageno 12a1b12bac feat: clean up base tuono example 2024-06-06 19:15:07 +02:00
Valerio Ageno c141e0b843 fix: CSS module metion in readme 2024-06-06 18:57:47 +02:00
Valerio Ageno 4d12fd9d20 Update README.md 2024-06-06 08:34:59 +02:00
Valerio Ageno 5322fe910a doc: update README.md 2024-06-05 21:17:26 +02:00
Valerio Ageno 70a6aa1a67 fix: package name and update version to v0.0.11 2024-06-05 09:06:48 +02:00
Valerio Ageno 8d2be4178b feat: ugrade turbo 2024-06-04 20:57:53 +02:00
Valerio Ageno e89f2f63f8 feat: update version to v0.0.10 2024-06-04 20:49:13 +02:00
Valerio Ageno f6e7ebd33e fix: correctly import modulepreload-polyfill 2024-06-04 20:48:19 +02:00
Valerio Ageno 5ea209de0b feat: prevent default code on route 2024-06-04 20:29:36 +02:00
Valerio Ageno a4b2dd222d feat: update crate version to v0.0.9 2024-06-04 19:18:30 +02:00
Valerio Ageno c84f5d479b fix: static consts locations 2024-06-04 19:17:31 +02:00
Valerio Ageno 9251d445d3 feat: update version to v0.0.8 2024-06-04 19:06:53 +02:00
Valerio Ageno c1ff7e36a8 feat: update static_files mod import 2024-06-04 19:05:58 +02:00
Valerio Ageno 8dd8123fb1 feat: update version to v0.0.7 2024-06-04 18:59:33 +02:00
Valerio Ageno 7040fbfb3d feat: copy readme on pipeline 2024-06-04 18:58:24 +02:00
Valerio Ageno ee2abbd4e1 feat: tuono new command 2024-06-04 18:55:15 +02:00
Valerio Ageno 252c463300 feat: resize README logo 2024-06-03 20:59:39 +02:00
Valerio Ageno a10f205ccd feat: create tuono main page 2024-06-02 21:24:43 +02:00
Valerio Ageno 7cfaf94146 feat: enable dynamic routes on frontend 2024-06-02 20:32:53 +02:00
Valerio Ageno 7557df10f3 feat: enable dynamic routes on frontend 2024-06-02 12:14:56 +02:00
Valerio Ageno 64c37f42c5 feat: handle dynamic routes creation 2024-05-31 20:51:24 +02:00
Valerio Ageno 00594b7c3a feat: pass reqwest client to handler 2024-05-30 21:03:25 +02:00
Valerio Ageno 930e7bc851 feat: filter reload trigger files 2024-05-30 21:03:09 +02:00
Valerio Ageno d7d0001299 feat: add TuonoProps type 2024-05-29 21:54:21 +02:00
Valerio Ageno 1c0b4bba04 feat: update source builder 2024-05-29 21:31:28 +02:00
Valerio Ageno c12739d526 feat: create base tuono example 2024-05-26 19:05:19 +02:00
Valerio Ageno 0780a6ec79 feat: prevent empty .tuono folder 2024-05-26 18:44:15 +02:00
Valerio Ageno 73e142a5d7 feat: allow passing zero arguments to tuono CLI 2024-05-26 18:26:36 +02:00
Valerio Ageno d3cee9f1b2 feat: update ssr_rs crate to v0.5.2 2024-05-25 18:04:43 +02:00
Valerio Ageno e6370cb7d4 feat: add tuono/README.md as root README symlink 2024-05-25 18:02:21 +02:00
Valerio Ageno e2e6e313a5 feat: update version to v0.0.6 2024-05-25 18:01:09 +02:00
Valerio Ageno 2348576fa4 feat: generate main file from tuono dev cmd 2024-05-25 17:59:27 +02:00
Valerio Ageno 9550f8243c chore: remove outdated READMEs 2024-05-25 15:03:31 +02:00
Valerio Ageno 1d87a78393 chore: merge packages into tuono 2024-05-25 14:56:55 +02:00
Valerio Ageno ee092194d1 fix: change version to v0.0.5 2024-05-25 12:15:49 +02:00
124 changed files with 2182 additions and 960 deletions
+3 -6
View File
@@ -25,9 +25,6 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Install turbo
run: pnpm install turbo --global
- name: Build
run: pnpm build
@@ -70,12 +67,12 @@ jobs:
version: 8
run_install: false
- name: Clone root README.md on tuono package
run: cp README.md packages/tuono/README.md
- name: Install dependencies
run: pnpm install
- name: Install turbo
run: pnpm install turbo --global
- name: Build
run: pnpm build
+3 -1
View File
@@ -6,5 +6,7 @@ members = [
"crates/tuono_lib_macros",
]
exclude = [
"examples/playground"
"examples/playground",
"examples/tuono",
"examples/tutorial"
]
+40 -20
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png">
<img src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="200px">
</p>
<h1 align="center">Tuono<br>The react/rust fullstack framework</h1>
<p align="center">
@@ -10,6 +10,22 @@
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
Why Tuono? Just a badass name.
> If you want to see how this project actually works check the [tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md) page.
## Introduction
NodeJs/Deno/Bun are the only tools that make a React app fullstack right? (no)
Tuono wants to prove that it's possible creating fully fledged react applications without the need to host them on a JS runtime server leveraging the best of the two worlds:
super powered server and amazing development experience.
## Requirements
- rust
- cargo
- node
- pnpm (other package managers support will be added soon)
## Installation
```
@@ -19,7 +35,7 @@ cargo install tuono
## Create a new project
```
tuono new
tuono new [NAME]
```
## Development
@@ -27,30 +43,34 @@ tuono new
```
tuono dev
```
## Features
- Rust based server side rendering
- FS routing
- CSS modules
- Hot Module Reload
- Multi thread backend
- [x] FS routing
- [x] Hot Module Reload
- [x] CSS modules
- [x] Rust based server side rendering
- [x] Multi thread backend
- [x] Development environment
- [ ] Create custom APIs
- [ ] Image optimization
- [ ] Build optimization
- [ ] Server streamed content
> 💡 Any suggestion or improvement is strongly appreciated
## Folder structure
```
- .tuono
- public/
| public/
- src/
| - routes/
| | - api/
| routes/
| styles/
| package.json
| Cargo.toml
| .gitignore
| tsconfig.json
```
```tsx
// Data is passed by the loading function
const IndexPage = ({data, isLoading, isError}) => <h1>Index Page</h1>
## License
export const Route = createFileRoute({
loader: (params) => fetchApi(params),
component: IndexPage
})
```
This project is licensed under the MIT License.
-24
View File
@@ -1,24 +0,0 @@
# Repository structure doc
## Monorepo tool
- [Turborepo](https://turbo.build/repo)
## Packages
> Highly inspired by [@tanstack/router](https://tanstack.com/router/latest)
- vite-fs-router
- router-generator
- react-router
- react-router-server
- tuono-runtime (entry point)
### JS Public interface
- tuono-runtime (all in one js framework package)
## Crates
- tuono (cli interface)
- tuono-axum-builder (axum project builder)
- tuono-watch (folder watch reload)
### Rust Public interface
- tuono (cli, dev and prod environment)
+6 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono"
version = "0.0.4"
version = "0.1.2"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
@@ -18,11 +18,15 @@ name = "tuono"
path = "src/lib.rs"
[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
clap = { version = "4.5.4", features = ["derive", "cargo"] }
watchexec = "4.0.0"
miette = "7.2.0"
watchexec-signals = "3.0.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0.202", features = ["derive"] }
watchexec-supervisor = "2.0.0"
glob = "0.3.1"
regex = "1.10.4"
reqwest = {version = "0.12.4", features =["blocking", "json"]}
serde_json = "1.0"
-161
View File
@@ -1,161 +0,0 @@
use glob::glob;
use std::collections::HashMap;
use std::fs;
use std::io::prelude::*;
use std::path::Path;
use std::path::PathBuf;
const ROOT_FOLDER: &'static str = "src/routes";
enum Mode {
Prod,
Dev,
}
const AXUM_ENTRY_POINT: &'static str = r##"
// File automatically generated
// Do not manually change it
use axum::extract::Request;
use axum::response::Html;
use axum::{routing::get, Router};
use tower_http::services::ServeDir;
use tuono_lib::{ssr, Ssr};
// MODULE_IMPORTS
#[tokio::main]
async fn main() {
Ssr::create_platform();
let app = Router::new()
// ROUTE_BUILDER
.fallback_service(ServeDir::new("public").fallback(get(catch_all)));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn catch_all(request: Request) -> Html<String> {
let pathname = &request.uri();
let headers = &request.headers();
let req = tuono_lib::Request::new(pathname, headers);
// TODO: remove unwrap
let payload = tuono_lib::Payload::new(&req, Box::new(""))
.client_payload()
.unwrap();
let result = ssr::Js::SSR.with(|ssr| ssr.borrow_mut().render_to_string(Some(&payload)));
match result {
Ok(html) => Html(html),
_ => Html("500 internal server error".to_string()),
}
}
"##;
fn create_main_file(base_path: &Path, bundled_file: &String) {
let mut data_file =
fs::File::create(base_path.join(".tuono/main.rs")).expect("creation failed");
data_file
.write(bundled_file.as_bytes())
.expect("write failed");
}
fn get_route_name<'a>(path: &PathBuf, base_path: &Path) -> String {
let base_path_str = base_path.to_str().unwrap();
if base_path_str == "." {
return path.to_str().unwrap().replace("/src/routes/", "");
}
// TODO: refactor this horrible code
if base_path_str.ends_with("/") {
return path
.to_str()
.unwrap()
.replace(&format!("{base_path_str}src/routes"), "")
.replace(".rs", "")
.replace("index", "");
}
path.to_str()
.unwrap()
.replace(&format!("{base_path_str}/src/routes"), "")
.replace(".rs", "")
.replace("index", "")
}
fn collect_handlers(base_path: &Path) -> HashMap<String, String> {
// <path, name>
let mut mods_map: HashMap<String, String> = HashMap::new();
let _: Vec<_> = glob(base_path.join("src/routes/**/*.rs").to_str().unwrap())
.unwrap()
.map(|entry| {
let entry = entry.unwrap();
let route_name = get_route_name(&entry, base_path);
// Remove first slash
let mut module = route_name.as_str().chars();
module.next();
mods_map.insert(
module.as_str().to_string(),
entry
.to_str()
.unwrap()
.replace(base_path.to_str().unwrap(), ""),
);
})
.collect();
dbg!(&mods_map);
return mods_map;
}
fn create_axum_routes_declaration(routes: &HashMap<String, String>) -> String {
let mut route_declarations = String::from("// ROUTE_BUILDER\n");
for (route, _path) in routes.iter() {
route_declarations.push_str(&format!(r#".route("/{route}", get({route}::route))"#));
route_declarations.push_str(&format!(
r#".route("/__tuono/data/{route}", get({route}::api))"#
));
}
route_declarations
}
fn create_modules_declaration(routes: &HashMap<String, String>) -> String {
let mut route_declarations = String::from("// MODULE_IMPORTS\n");
for (route, path) in routes.iter() {
route_declarations.push_str(&format!(
r#"#[path="..{path}"]
mod {route};
"#
))
}
route_declarations
}
pub fn bundle_axum_source() {
println!("Axum project bundling");
let base_path = std::env::current_dir().unwrap();
let mods = collect_handlers(&base_path);
let bundled_file = AXUM_ENTRY_POINT
.replace("// ROUTE_BUILDER\n", &create_axum_routes_declaration(&mods))
.replace("// MODULE_IMPORTS\n", &create_modules_declaration(&mods));
create_main_file(&base_path, &bundled_file);
}
+31 -15
View File
@@ -1,6 +1,11 @@
use clap::{Parser, Subcommand};
use std::process::Command;
mod axum_source_builder;
mod source_builder;
use source_builder::{bundle_axum_source, create_client_entry_files};
use crate::source_builder::check_tuono_folder;
mod scaffold_project;
mod watch;
#[derive(Subcommand, Debug)]
@@ -9,31 +14,42 @@ enum Actions {
Dev,
/// Build the production assets
Build,
/// Create a new project folder
New,
/// Scaffold a new project
New { folder_name: Option<String> },
}
#[derive(Parser, Debug)]
#[command(version, about = "The react/rust fullstack framework")]
struct Args {
#[command(subcommand)]
action: Option<Actions>,
action: Actions,
}
pub fn cli() {
fn init_tuono_folder() -> std::io::Result<()> {
check_tuono_folder()?;
bundle_axum_source()?;
create_client_entry_files()?;
Ok(())
}
pub fn cli() -> std::io::Result<()> {
let args = Args::parse();
match args.action.unwrap() {
Actions::Dev => watch::watch().unwrap(),
Actions::Build => {
axum_source_builder::bundle_axum_source();
println!("Build JS source");
let mut vite_build = Command::new("./node_modules/.bin/tuono-build-prod");
let _ = &vite_build.output().unwrap();
match args.action {
Actions::Dev => {
init_tuono_folder()?;
watch::watch().unwrap();
}
Actions::New => {
println!("Scaffold new project")
Actions::Build => {
init_tuono_folder()?;
let mut vite_build = Command::new("./node_modules/.bin/tuono-build-prod");
let _ = &vite_build.output()?;
}
Actions::New { folder_name } => {
scaffold_project::create_new_project(folder_name);
}
}
Ok(())
}
+1 -1
View File
@@ -1,5 +1,5 @@
use tuono::cli;
fn main() {
cli();
cli().unwrap();
}
+165
View File
@@ -0,0 +1,165 @@
use clap::crate_version;
use reqwest::blocking;
use serde::Deserialize;
use std::env;
use std::fs::{self, create_dir, File, OpenOptions};
use std::io::{self, prelude::*};
use std::path::{Path, PathBuf};
const GITHUB_TUONO_REPO_URL: &str =
"https://api.github.com/repos/Valerioageno/tuono/git/trees/main?recursive=1";
const GITHUB_RAW_CONTENT_URL: &str = "https://raw.githubusercontent.com/Valerioageno/tuono/main/";
#[derive(Deserialize, Debug)]
enum GithubFileType {
#[serde(rename = "blob")]
Blob,
#[serde(rename = "tree")]
Tree,
}
#[derive(Deserialize, Debug)]
struct GithubResponse<T> {
tree: Vec<T>,
}
#[derive(Deserialize, Debug)]
struct GithubFile {
path: String,
#[serde(rename(deserialize = "type"))]
element_type: GithubFileType,
}
fn create_file(path: PathBuf, content: String) -> std::io::Result<()> {
let mut file = File::create(path)?;
let _ = file.write_all(content.as_bytes());
Ok(())
}
pub fn create_new_project(folder_name: Option<String>) {
let folder = folder_name.unwrap_or(".".to_string());
if folder != "." {
create_dir(&folder).unwrap();
}
let client = blocking::Client::builder()
.user_agent("")
.build()
.expect("Failed to build reqwest client");
let res = client
.get(GITHUB_TUONO_REPO_URL)
.send()
.expect("Failed to call the folder github API")
.json::<GithubResponse<GithubFile>>()
.expect("Failed to parse the repo structure");
let new_project_files = res
.tree
.iter()
.filter(|GithubFile { path, .. }| {
// TODO: Handle custom example download by CLI argument --template
if path.starts_with("examples/tuono/") {
return true;
}
false
})
.collect::<Vec<&GithubFile>>();
let folder_name = PathBuf::from(&folder);
let current_dir = env::current_dir().expect("Failed to get current working directory");
let folder_path = current_dir.join(folder_name);
create_directories(&new_project_files, &folder_path).expect("Failed to create directories");
for GithubFile {
element_type, path, ..
} in new_project_files.iter()
{
if let GithubFileType::Blob = element_type {
let file_content = client
.get(format!("{GITHUB_RAW_CONTENT_URL}{path}"))
.send()
.expect("Failed to call the folder github API")
.text()
.expect("Failed to parse the repo structure");
let path = PathBuf::from(&path.replace("examples/tuono/", ""));
let file_path = folder_path.join(&path);
create_file(file_path, file_content).expect("failed to create file");
}
}
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");
outro(folder);
}
fn create_directories(new_project_files: &Vec<&GithubFile>, folder_path: &Path) -> io::Result<()> {
for GithubFile {
element_type, path, ..
} in new_project_files.iter()
{
if let GithubFileType::Tree = element_type {
let path = PathBuf::from(&path.replace("examples/tuono/", ""));
let dir_path = folder_path.join(&path);
create_dir(&dir_path).unwrap();
}
}
Ok(())
}
fn update_package_json_version(folder_path: &Path) -> io::Result<()> {
let v = crate_version!();
let package_json_path = folder_path.join(PathBuf::from("package.json"));
let package_json = fs::read_to_string(&package_json_path)?;
let package_json = package_json.replace("link:../../packages/tuono", v);
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(package_json_path)?;
file.write_all(package_json.as_bytes())?;
Ok(())
}
fn update_cargo_toml_version(folder_path: &Path) -> io::Result<()> {
let v = crate_version!();
let cargo_toml_path = folder_path.join(PathBuf::from("Cargo.toml"));
let cargo_toml = fs::read_to_string(&cargo_toml_path)?;
let cargo_toml =
cargo_toml.replace("{ path = \"../../crates/tuono_lib/\"}", &format!("\"{v}\""));
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(cargo_toml_path)?;
file.write_all(cargo_toml.as_bytes())?;
Ok(())
}
fn outro(folder_name: String) {
println!("Success! 🎉");
if folder_name != "." {
println!("\nGo to the project directory:");
println!("cd {folder_name}/");
}
println!("\nInstall the dependencies:");
println!("pnpm install");
println!("\nRun the local environment:");
println!("tuono dev");
}
+358
View File
@@ -0,0 +1,358 @@
use glob::glob;
use glob::GlobError;
use regex::Regex;
use std::collections::HashMap;
use std::fs;
use std::io;
use std::io::prelude::*;
use std::path::Path;
use std::path::PathBuf;
pub 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)
";
pub 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 '../src/styles/global.css'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
hydrate(routeTree)
";
pub const AXUM_ENTRY_POINT: &str = r##"
// File automatically generated
// Do not manually change it
use axum::extract::{Path, Request};
use axum::response::Html;
use axum::{routing::get, Router};
use tower_http::services::ServeDir;
use std::collections::HashMap;
use tuono_lib::{ssr, Ssr};
use reqwest::Client;
// MODULE_IMPORTS
#[tokio::main]
async fn main() {
Ssr::create_platform();
let fetch = Client::new();
let app = Router::new()
// ROUTE_BUILDER
.fallback_service(ServeDir::new("public").fallback(get(catch_all)))
.with_state(fetch);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn catch_all(Path(params): Path<HashMap<String, String>>, request: Request) -> Html<String> {
let pathname = &request.uri();
let headers = &request.headers();
let req = tuono_lib::Request::new(pathname, headers, params);
// TODO: remove unwrap
let payload = tuono_lib::Payload::new(&req, Box::new(""))
.client_payload()
.unwrap();
let result = ssr::Js::SSR.with(|ssr| ssr.borrow_mut().render_to_string(Some(&payload)));
match result {
Ok(html) => Html(html),
_ => Html("500 internal server error".to_string()),
}
}
"##;
const ROOT_FOLDER: &str = "src/routes";
const DEV_FOLDER: &str = ".tuono";
pub enum Mode {
Prod,
Dev,
}
#[derive(Debug, PartialEq, Eq)]
struct Route {
/// Every module import is the path with a _ instead of /
pub module_import: String,
pub axum_route: String,
}
fn has_dynamic_path(route: &str) -> bool {
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
regex.is_match(route)
}
impl Route {
pub fn new(path: &str) -> Self {
let route_name = path.replace(".rs", "");
// Remove first slash
let mut module = route_name.as_str().chars();
module.next();
let axum_route = path.replace("/index.rs", "").replace(".rs", "");
if axum_route.is_empty() {
return Route {
module_import: module.as_str().to_string().replace('/', "_"),
axum_route: "/".to_string(),
};
}
if has_dynamic_path(&route_name) {
return Route {
module_import: module
.as_str()
.to_string()
.replace('/', "_")
.replace('[', "dyn_")
.replace(']', ""),
axum_route: axum_route.replace('[', ":").replace(']', ""),
};
}
Route {
module_import: module.as_str().to_string().replace('/', "_"),
axum_route,
}
}
}
struct SourceBuilder {
route_map: HashMap<PathBuf, Route>,
mode: Mode,
base_path: PathBuf,
}
impl SourceBuilder {
pub fn new(mode: Mode) -> Self {
let base_path = std::env::current_dir().unwrap();
SourceBuilder {
route_map: HashMap::new(),
mode,
base_path,
}
}
fn collect_routes(&mut self) {
glob(self.base_path.join("src/routes/**/*.rs").to_str().unwrap())
.unwrap()
.for_each(|entry| self.collect_route(entry))
}
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
let entry = path_buf.unwrap();
let base_path_str = self.base_path.to_str().unwrap();
let path = entry
.to_str()
.unwrap()
.replace(&format!("{base_path_str}/src/routes"), "");
let route = Route::new(&path);
self.route_map.insert(PathBuf::from(path), route);
}
}
fn create_main_file(base_path: &Path, bundled_file: &String) {
let mut data_file =
fs::File::create(base_path.join(".tuono/main.rs")).expect("creation failed");
data_file
.write_all(bundled_file.as_bytes())
.expect("write failed");
}
fn create_routes_declaration(routes: &HashMap<PathBuf, Route>) -> String {
let mut route_declarations = String::from("// ROUTE_BUILDER\n");
for (_, route) in routes.iter() {
let Route {
axum_route,
module_import,
} = &route;
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::route))"#
));
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}", get({module_import}::api))"#
));
}
route_declarations
}
fn create_modules_declaration(routes: &HashMap<PathBuf, Route>) -> String {
let mut route_declarations = String::from("// MODULE_IMPORTS\n");
for (path, route) in routes.iter() {
let module_name = &route.module_import;
let path_str = path.to_str().unwrap();
route_declarations.push_str(&format!(
r#"#[path="../{ROOT_FOLDER}{path_str}"]
mod {module_name};
"#
))
}
route_declarations
}
pub fn bundle_axum_source() -> io::Result<()> {
let base_path = std::env::current_dir().unwrap();
let mut source_builder = SourceBuilder::new(Mode::Dev);
source_builder.collect_routes();
let bundled_file = AXUM_ENTRY_POINT
.replace(
"// ROUTE_BUILDER\n",
&create_routes_declaration(&source_builder.route_map),
)
.replace(
"// MODULE_IMPORTS\n",
&create_modules_declaration(&source_builder.route_map),
);
create_main_file(&base_path, &bundled_file);
Ok(())
}
pub fn check_tuono_folder() -> io::Result<()> {
let dev_folder = Path::new(DEV_FOLDER);
if !&dev_folder.is_dir() {
println!("exists");
fs::create_dir(dev_folder)?;
}
Ok(())
}
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 {
use super::*;
#[test]
fn find_dynamic_paths() {
let routes = [
("/home/user/Documents/tuono/src/routes/about.rs", false),
("/home/user/Documents/tuono/src/routes/index.rs", false),
(
"/home/user/Documents/tuono/src/routes/posts/index.rs",
false,
),
(
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
true,
),
];
routes
.into_iter()
.for_each(|route| assert_eq!(has_dynamic_path(route.0), route.1));
}
#[test]
fn collect_routes() {
let mut source_builder = SourceBuilder::new(Mode::Dev);
source_builder.base_path = "/home/user/Documents/tuono".into();
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/index.rs",
"/home/user/Documents/tuono/src/routes/posts/index.rs",
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
];
routes
.into_iter()
.for_each(|route| source_builder.collect_route(Ok(PathBuf::from(route))));
let results = [
("/index.rs", "index"),
("/about.rs", "about"),
("/posts/index.rs", "posts_index"),
("/posts/[post].rs", "posts_dyn_post"),
];
results.into_iter().for_each(|(path, module_import)| {
assert_eq!(
source_builder
.route_map
.get(&PathBuf::from(path))
.unwrap()
.module_import,
String::from(module_import)
)
})
}
#[test]
fn create_multi_level_axum_paths() {
let mut source_builder = SourceBuilder::new(Mode::Dev);
source_builder.base_path = "/home/user/Documents/tuono".into();
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/index.rs",
"/home/user/Documents/tuono/src/routes/posts/index.rs",
"/home/user/Documents/tuono/src/routes/posts/any-post.rs",
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
];
routes
.into_iter()
.for_each(|route| source_builder.collect_route(Ok(PathBuf::from(route))));
let results = [
("/index.rs", "/"),
("/about.rs", "/about"),
("/posts/index.rs", "/posts"),
("/posts/any-post.rs", "/posts/any-post"),
("/posts/[post].rs", "/posts/:post"),
];
results.into_iter().for_each(|(path, expected_path)| {
assert_eq!(
source_builder
.route_map
.get(&PathBuf::from(path))
.unwrap()
.axum_route,
String::from(expected_path)
)
})
}
}
+53 -39
View File
@@ -4,69 +4,83 @@ use watchexec_supervisor::command::{Command, Program};
use miette::{IntoDiagnostic, Result};
use watchexec::Watchexec;
use watchexec_signals::Signal;
use watchexec_supervisor::job::start_job;
use watchexec_supervisor::job::{start_job, Job};
use crate::axum_source_builder::bundle_axum_source;
use crate::source_builder::bundle_axum_source;
// What is the development pipeline?
//
// 1. Any file gets updates (rs/js/ts)
// 2. Client side vite works separately
// 3. Stop the dev server
// 4. Build the ssr client bundle
// 5. Restart the server
//
//
// The current solution is not optimized
// - We should avoid to bundle static lib (i.e. react) in the main bundle
#[tokio::main]
pub async fn watch() -> Result<()> {
bundle_axum_source();
let (watch_client, _) = start_job(Arc::new(Command {
fn watch_react_src() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "node_modules/.bin/tuono-dev-watch".into(),
args: vec![],
}
.into(),
},
options: Default::default(),
}));
}))
.0
}
watch_client.start().await;
let (run_server, _) = start_job(Arc::new(Command {
fn build_rust_src() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "cargo".into(),
args: vec!["run".to_string()],
}
.into(),
args: vec!["run".to_string(), "-q".to_string()],
},
options: Default::default(),
}));
}))
.0
}
let (build_ssr_bundle, _) = start_job(Arc::new(Command {
fn build_react_ssr_src() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "node_modules/.bin/tuono-dev-ssr".into(),
args: vec![],
}
.into(),
},
options: Default::default(),
}));
}))
.0
}
#[tokio::main]
pub async fn watch() -> Result<()> {
println!("Starting development environment...");
watch_react_src().start().await;
let run_server = build_rust_src();
let build_ssr_bundle = build_react_ssr_src();
build_ssr_bundle.start().await;
run_server.start().await;
println!("\nDevelopment app ready at http://localhost:3000/");
let wx = Watchexec::new(move |mut action| {
let mut should_reload = false;
for event in action.events.iter() {
for _ in event.paths() {
run_server.stop();
println!("Reloading server...");
build_ssr_bundle.stop();
build_ssr_bundle.start();
bundle_axum_source();
run_server.start();
for path in event.paths() {
if path.0.to_string_lossy().ends_with(".rs")
// Either tsx and jsx
|| path.0.to_string_lossy().ends_with("sx")
{
should_reload = true
}
}
}
if should_reload {
println!("Reloading...");
run_server.stop();
build_ssr_bundle.stop();
build_ssr_bundle.start();
bundle_axum_source().expect("Failed to bunlde rust source");
run_server.start();
println!("Ready!");
should_reload = false;
}
// if Ctrl-C is received, quit
if action.signals().any(|sig| sig == Signal::Interrupt) {
action.quit();
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib"
version = "0.0.4"
version = "0.1.2"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
@@ -18,10 +18,10 @@ name = "tuono_lib"
path = "src/lib.rs"
[dependencies]
ssr_rs = "0.5.1"
ssr_rs = "0.5.2"
axum = "0.7.5"
serde = { version = "1.0.202", features = ["derive"] }
erased-serde = "0.4.5"
serde_json = "1.0"
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.0.4"}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.1.2"}
+1 -1
View File
@@ -1,3 +1,3 @@
# Tuonolib
# tuono_lib
This project exposes the interfaces to easily handle the backend build.
+14 -5
View File
@@ -3,10 +3,11 @@ use std::collections::HashMap;
use axum::http::{HeaderMap, Uri};
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Request<'a> {
uri: &'a Uri,
headers: &'a HeaderMap,
pub headers: &'a HeaderMap,
pub params: HashMap<String, String>,
}
/// Location must match client side interface
@@ -21,15 +22,23 @@ pub struct Location<'a> {
}
impl<'a> Request<'a> {
pub fn new(uri: &'a Uri, headers: &'a HeaderMap) -> Request<'a> {
Request { uri, headers }
pub fn new(
uri: &'a Uri,
headers: &'a HeaderMap,
params: HashMap<String, String>,
) -> Request<'a> {
Request {
uri,
headers,
params,
}
}
pub fn location(&self) -> Location<'a> {
Location {
href: self.uri.to_string(),
pathname: &self.uri.path(),
// TODO: hanler search map
// TODO: handler search map
search: HashMap::new(),
search_str: &self.uri.query().unwrap_or(""),
hash: "",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib_macros"
version = "0.0.4"
version = "0.1.2"
edition = "2021"
description = "The react/rust fullstack framework"
homepage = "https://github.com/Valerioageno/tuono"
+3
View File
@@ -0,0 +1,3 @@
# tuono_lib_macros
Macros for tuono_lib
+18 -9
View File
@@ -9,25 +9,30 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
quote! {
use axum::response::{Html, IntoResponse};
use std::collections::HashMap;
use axum::extract::{State, Path};
use reqwest::Client;
#item
pub async fn route(request: axum::extract::Request) -> Html<String> {
pub async fn route(
Path(params): Path<HashMap<String, String>>,
State(client): State<Client>,
request: axum::extract::Request
) -> Html<String> {
let pathname = &request.uri();
let headers = &request.headers();
let req = tuono_lib::Request::new(pathname, headers);
let req = tuono_lib::Request::new(pathname, headers, params);
let local_response = #fn_name(&req);
let local_response = #fn_name(req.clone(), client).await;
let res = match local_response{
let res = match local_response {
tuono_lib::Response::Props(val) => {
// TODO: remove unwrap
let payload = tuono_lib::Payload::new(&req, val).client_payload().unwrap();
dbg!(&payload);
tuono_lib::ssr::Js::SSR.with(|ssr| ssr.borrow_mut().render_to_string(Some(&payload)))
},
/// TODO: handle here redirection and rewrite
@@ -40,13 +45,17 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
}
}
pub async fn api(request: axum::extract::Request) -> axum::response::Response {
pub async fn api(
Path(params): Path<HashMap<String, String>>,
State(client): State<Client>,
request: axum::extract::Request
) -> axum::response::Response {
let pathname = &request.uri();
let headers = &request.headers();
let req = tuono_lib::Request::new(pathname, headers);
let req = tuono_lib::Request::new(pathname, headers, params);
let local_response = #fn_name(&req);
let local_response = #fn_name(req.clone(), client).await;
let res = match local_response{
tuono_lib::Response::Props(val) => return axum::Json(val).into_response(),
+446
View File
@@ -0,0 +1,446 @@
# Tuono tutorial
This tutorial is meant for giving you a sneak peek of the framework and is intended to evolve along the development - be sure to have installed the latest version.
The first part is about the project setup and the base knowledge needed to work with tuono. The actual tutorial starts at [Tutorial introduction](#tutorial-introduction).
> If you prefer to see directly the final outcome check out the [tutorial source](https://github.com/Valerioageno/tuono/tree/main/examples/tutorial).
## Installation
The tuono CLI is hosted on [crates.io](https://crates.io/crates/tuono); to download and install it just run on a terminal:
```bash
$ cargo install tuono
```
To check that is correctly installed run:
```bash
$ tuono --version
```
## Project scaffold
To setup a new project you just need to run the following command:
```bash
$ tuono new tuono-tutorial
```
Get into the project folder and install the dependencies with:
```bash
$ pnpm install
```
Open it with your favourite code editor.
The project will have the following structure:
```bash
| public/
- src/
| routes/
| styles/
| package.json
| Cargo.toml
| .gitignore
| tsconfig.json
```
**public/**: put here all the files you want to be public
**src/routes/**: All the files in this folder are considered routes. All the routes are server side rendered out of the box. To add server side capabilities just create a rust file with the same name as the route (i.e. `about.tsx``about.rs`).
**src/styles/**: In this folder there is the `global.css` file that stores all the global styles. For the rest of the project you can use CSS modules (⚠️ CSS modules on routes are forbidden).
## Start the dev environment
To start the development environment you just need to run the following command within the project folder:
```bash
$ tuono dev
```
The first time might take a little bit because it will install all the rusts dependencies. All the other execution will be pretty quick!
> 💡 The `tuono dev` development script is currently under strong optimization improvements. In case you face any error delete the cache `.tuono` folder and run it again!
Then open [`http://localhost:3000/`](http://localhost:3000/) on the browser.
## The “/” route
All the `index.tsx` files represent the folder root page (i.e. `src/routes/posts/index.tsx` is [`http://localhost:3000/posts`](http://localhost:3000/posts) as well as `src/routes/posts.tsx`).
The file `index.rs` represents the server side capabilities for the index route. On this file you can:
- Passing server side props
- Redirect/Rewrite to a different route (Available soon)
- Changing http status code (Available soon)
## Tutorial introduction
Now that we have some knowledge about the project structure lets start the real tutorial.
The goal is to use the [PokeAPI](https://pokeapi.co/docs/v2) to list all the pokemons of the first generation (the best one btw) and then reserve a dynamic page for each one separately.
## Fetch all the pokemons
To start lets fetch all of them in the root page; since we want to render them on the server side we gonna need to implement the logic in the `index.rs` file.
Clear the `index.rs` file and paste:
```rust
// src/routes/index.rs
use serde::{Deserialize, Serialize};
use tuono_lib::{Request, Response};
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
#[derive(Debug, Serialize, Deserialize)]
struct Pokemons {
results: Vec<Pokemon>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Pokemon {
name: String,
url: String,
}
#[tuono_lib::handler]
async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response {
return match fetch.get(ALL_POKEMON).send().await {
Ok(res) => {
let data = res.json::<Pokemons>().await.unwrap();
Response::Props(Box::new(data))
}
Err(_err) => Response::Props(Box::new(Pokemons { results: vec![] })),
};
}
```
Now the pokemons are correctly fetched and hydrated on the client side so we can actually use them. Clear the `index.tsx` file and paste:
```tsx
// src/routes/index.tsx
import type { TuonoProps } from "tuono";
interface Pokemon {
name: string
}
interface IndexProps {
results: Pokemon[]
}
export default function IndexPage({
data,
}: TuonoProps<IndexProps>): JSX.Element {
if (!data?.results) {
return <></>;
}
return (
<>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
{data.results.map((pokemon) => {
return pokemon.name;
})}
</ul>
</>
);
}
```
Refresh now the browser! A bit ugly but all the pokemons are finally printed on screen!
## Create a stand-alone component
Lets then create the button needed for displaying the list of pokemons.
Create the following file `src/components/PokemonLink.tsx` and fill the content with:
```tsx
// src/components/PokemonLink.tsx
import { Link } from "tuono";
interface Pokemon {
name: string
}
export default function PokemonLink({
pokemon,
id,
}: {
pokemon: Pokemon;
id: number;
}): JSX.Element {
return (
<Link href={`/pokemons/${pokemon.name}`}>
{pokemon.name}
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
/>
</Link>
);
}
```
Now that the link is done lets import it in the `index.tsx` file
```diff
// src/routes/index.tsx
++ import PokemonLink from '../components/PokemonLink'
// ...
<ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
-- {pokemons.map((pokemon) => {
-- return pokemon.name;
++ {pokemons.map((pokemon, i) => {
++ return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />;
})}
</ul>
// ...
```
Now the links work. Clicking on any of them we get redirected to the 404 page because we havent yet implemented the `pokemons/[pokemon]` page.
As previously said CSS modules are enabled out of the box so lets make those links a little bit nicer.
Create alongside the `PokemonLink.tsx` component the CSS module `PokemonLink.module.css` and copy the following content into it:
```css
/* src/components/PokemonLink.module.css */
.link {
width: 100%;
max-width: 216px;
position: relative;
background: white;
margin-bottom: 10px;
border: solid #f0f0f0 1px;
text-decoration: none;
color: black;
padding: 5px 5px 5px 15px;
border-radius: 10px;
display: flex;
justify-content: space-between;
transition: 0.2s;
align-items: center;
}
.link:hover {
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
}
.link img {
width: 70px;
background: white;
border-radius: 50%;
}
```
> 💡 SASS is supported out of the box. Just install the processor in the devDependencies `pnpm i -D sass` and run again `tuono dev`
Then import the styles into the `PokemonLink` component as following:
```diff
// src/components/PokemonLink.tsx
import { Link } from "tuono";
import type { Pokemon } from "./../types/pokemon";
++ import styles from './PokemonLink.module.css'
export default function PokemonLink({
pokemon,
id,
}: {
pokemon: Pokemon;
id: number;
}): JSX.Element {
return (
-- <Link href={`/pokemons/${pokemon.name}`}>
++ <Link className={styles.link} href={`/pokemons/${pokemon.name}`}>
{pokemon.name}
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
/>
</Link>
);
}
```
## Create the `/pokemons/[pokemon]` route
The homepage is ready. We have the full list of pokemons and they are all links. Great!
Now we want to make those links actually pointing to a real page. Lets create the dynamic route.
Create the folder `routes/pokemons` and then create the two files `[pokemon].tsx` and `[pokemon].rs`.
These two will handle every requests that points to `http://localhost:3000/pokemons/bulbasaur..mew`.
Lets first work on the server side file. Paste into the new `[pokemon].rs` file the following code:
```rust
use serde::{Deserialize, Serialize};
use tuono_lib::{Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
#[derive(Debug, Serialize, Deserialize)]
struct Pokemon {
name: String,
id: u16,
weight: u16,
height: u16,
}
#[tuono_lib::handler]
async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response {
// The param `pokemon` is defined in the route filename [pokemon].rs
let pokemon = req.params.get("pokemon").unwrap();
return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
Ok(res) => {
let data = res.json::<Pokemon>().await.unwrap();
Response::Props(Box::new(data))
}
Err(_err) => Response::Props(Box::new(Pokemon {
name: "Nope".to_string(),
id: 0,
weight: 0,
height: 0,
})),
};
}
```
Then lets work on the frontend. Paste into the `[pokemon].tsx` file the following code:
```tsx
import { TuonoProps } from "tuono";
import PokemonView from "../../components/PokemonView";
export default function Pokemon({ data }: TuonoProps): JSX.Element {
return <PokemonView pokemon={data} />;
}
```
The browser should complain that the component `PokemonView` does not exist. Lets create it then!
```tsx
// components/PokemonView.tsx
import { Link } from "tuono";
import styles from "./PokemonView.module.css";
interface Pokemon {
name: string
id: string
weight: number
height: number
}
export default function PokemonView({
pokemon,
}: {
pokemon?: Pokemon;
}): JSX.Element {
return (
<div>
<Link className={styles["back-btn"]} href="/">
Back
</Link>
{pokemon?.name && (
<div className={styles.pokemon}>
<div>
<h1 className={styles.name}>
{pokemon.name}
</h1>
<dl className={styles.spec}>
<dt className={styles.label}>Weight:</dt>
<dd>{pokemon.weight}lbs</dd>
</dl>
<dl className={styles.spec}>
<dt className={styles.label}>Height:</dt>
<dd>{pokemon.height}ft</dd>
</dl>
</div>
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${pokemon.id}.png`}
/>
</div>
)}
</div>
);
}
```
```css
/* components/PokemonView.module.css */
.back-btn {
background-color: white;
border-radius: 10px;
padding: 7px 15px;
color: black;
text-decoration: none;
border: solid #f0f0f0 1px;
font-size: 20px;
}
.back-btn:hover {
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
}
.pokemon {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.name {
font-size: 50px;
font-weight: 700;
}
.pokemon img {
width: 400px;
}
.spec {
display: flex;
font-size: 18px;
margin-top: 10px;
}
.label {
font-weight: 700;
}
```
## Conclusion
Thats it! You just created a multi thread full stack application with rust and react.
The project is still under heavy development and the script for the production build (`tuono build`) is not ready yet but
I hope you got the taste of what is like working with rust and react in the same stack.
+9
View File
@@ -0,0 +1,9 @@
# Tuono examples
This folder includes all the official tuono starters.
To simply scaffold the base project run in your terminal:
```shell
$ tuono new [NAME]
```
+13
View File
@@ -0,0 +1,13 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.tuono
out
target
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "tuono"
version = "0.0.1"
edition = "2021"
[[bin]]
name = "tuono"
path = ".tuono/main.rs"
[dependencies]
axum = {version = "0.7.5", features = ["json"]}
tokio = { version = "1.37.0", features = ["full"] }
tower-http = {version = "0.5.2", features = ["fs"]}
serde = { version = "1.0.202", features = ["derive"] }
tuono_lib = { path = "../../crates/tuono_lib/"}
serde_json = "1.0"
reqwest = {version = "0.12.4", features = ["json"]}
+9 -1
View File
@@ -1 +1,9 @@
# TODO: Basic Tuono app
# Tuono starter
This is the starter tuono project. To download it run in your terminal:
```shell
$ tuono new [NAME]
```
+15
View File
@@ -0,0 +1,15 @@
{
"name": "tuono",
"description": "The react/rust fullstack framework",
"version": "0.0.1",
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1",
"tuono": "link:../../packages/tuono"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"typescript": "^5.4.5"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="82" height="73" viewBox="0 0 82 73" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M81.337 36.6169C81.337 31.245 74.5945 26.1541 64.2571 22.9971C66.6426 12.4847 65.5824 4.1211 60.9107 1.44343C59.8339 0.815331 58.5748 0.517811 57.1998 0.517811V4.20375C57.9619 4.20375 58.5748 4.35251 59.0884 4.6335C61.3414 5.92275 62.3188 10.8318 61.5568 17.1458C61.3745 18.6996 61.0763 20.3359 60.7119 22.0053C57.4649 21.212 53.9197 20.6004 50.1923 20.2037C47.9558 17.1459 45.6365 14.369 43.3007 11.9393C48.7013 6.93101 53.7706 4.18722 57.2164 4.18722V0.501282C52.6607 0.501282 46.6967 3.74094 40.6666 9.36076C34.6364 3.774 28.6726 0.567398 24.1168 0.567398V4.25333C27.5461 4.25333 32.6319 6.9806 38.0326 11.9558C35.7133 14.3855 33.394 17.1459 31.1907 20.2037C27.4467 20.6004 23.9015 21.212 20.6545 22.0219C20.2735 20.369 19.9918 18.7657 19.793 17.2285C19.0144 10.9145 19.9752 6.0054 22.2117 4.69962C22.7087 4.4021 23.3548 4.26987 24.1168 4.26987V0.583927C22.7252 0.583927 21.4662 0.881447 20.3728 1.50954C15.7177 4.18722 14.674 12.5343 17.0761 23.0136C6.77188 26.1871 0.0625 31.2615 0.0625 36.6169C0.0625 41.9887 6.805 47.0796 17.1424 50.2366C14.7568 60.749 15.8171 69.1126 20.4888 71.7903C21.5656 72.4184 22.8246 72.7159 24.2162 72.7159C28.772 72.7159 34.7359 69.4762 40.766 63.8564C46.7962 69.4432 52.76 72.6498 57.3158 72.6498C58.7073 72.6498 59.9664 72.3523 61.0598 71.7242C65.7149 69.0465 66.7586 60.6994 64.3565 50.2201C74.6276 47.0631 81.337 41.9722 81.337 36.6169ZM59.7676 25.5921C59.1547 27.7243 58.3926 29.9227 57.5311 32.121C56.8519 30.7987 56.1396 29.4764 55.361 28.1541C54.5989 26.8318 53.7872 25.5425 52.9754 24.2863C55.3278 24.6334 57.5974 25.0632 59.7676 25.5921ZM52.1802 43.1953C50.888 45.4267 49.5628 47.5424 48.1877 49.5094C45.7194 49.7243 43.2178 49.84 40.6997 49.84C38.1982 49.84 35.6967 49.7242 33.2449 49.5259C31.8699 47.559 30.528 45.4598 29.2358 43.2449C27.9768 41.0797 26.8337 38.8813 25.79 36.6664C26.8172 34.4516 27.9768 32.2367 29.2193 30.0714C30.5114 27.84 31.8368 25.7243 33.2118 23.7574C35.6801 23.5425 38.1817 23.4268 40.6997 23.4268C43.2013 23.4268 45.7028 23.5425 48.1546 23.7409C49.5296 25.7078 50.8715 27.807 52.1637 30.0218C53.4227 32.1871 54.5658 34.3855 55.6094 36.6003C54.5658 38.8152 53.4227 41.0301 52.1802 43.1953ZM57.5311 41.0466C58.4257 43.2615 59.1878 45.4763 59.8173 47.6251C57.6471 48.154 55.361 48.6003 52.992 48.9474C53.8037 47.6747 54.6155 46.3689 55.3775 45.0301C56.1396 43.7077 56.8519 42.3689 57.5311 41.0466ZM40.7329 58.6829C39.1922 57.0961 37.6515 55.3275 36.1274 53.3937C37.6184 53.4598 39.1425 53.5094 40.6832 53.5094C42.2404 53.5094 43.7811 53.4763 45.2886 53.3937C43.7976 55.3275 42.257 57.0961 40.7329 58.6829ZM28.4075 48.9474C26.0551 48.6003 23.7855 48.1705 21.6153 47.6416C22.2283 45.5094 22.9903 43.311 23.8518 41.1127C24.531 42.435 25.2434 43.7573 26.022 45.0796C26.8006 46.402 27.5958 47.6912 28.4075 48.9474ZM40.6501 14.5508C42.1907 16.1376 43.7314 17.9062 45.2555 19.8401C43.7645 19.7739 42.2404 19.7244 40.6997 19.7244C39.1425 19.7244 37.6018 19.7574 36.0943 19.8401C37.5853 17.9062 39.126 16.1376 40.6501 14.5508ZM28.391 24.2863C27.5792 25.559 26.7675 26.8648 26.0054 28.2037C25.2433 29.526 24.531 30.8483 23.8518 32.1706C22.9572 29.9557 22.1951 27.7409 21.5656 25.5921C23.7358 25.0797 26.022 24.6334 28.391 24.2863ZM13.3984 44.9805C7.53392 42.4846 3.74023 39.2119 3.74023 36.6169C3.74023 34.0218 7.53392 30.7326 13.3984 28.2533C14.8231 27.6417 16.3803 27.0962 17.9873 26.5838C18.9316 29.8235 20.1741 33.1954 21.7147 36.6499C20.1906 40.0879 18.9647 43.4433 18.037 46.6664C16.3969 46.154 14.8397 45.592 13.3984 44.9805ZM22.3111 68.6002C20.0581 67.311 19.0807 62.4019 19.8427 56.0879C20.0249 54.5341 20.3231 52.8978 20.6876 51.2284C23.9346 52.0218 27.4798 52.6333 31.2072 53.03C33.4437 56.0879 35.763 58.8647 38.0988 61.2945C32.6982 66.3027 27.6289 69.0465 24.1831 69.0465C23.4376 69.03 22.8081 68.8812 22.3111 68.6002ZM61.6065 56.0052C62.3851 62.3192 61.4242 67.2283 59.1878 68.5341C58.6908 68.8316 58.0447 68.9639 57.2827 68.9639C53.8534 68.9639 48.7675 66.2366 43.3669 61.2614C45.6862 58.8317 48.0055 56.0713 50.2088 53.0135C53.9528 52.6168 57.498 52.0052 60.745 51.1953C61.1261 52.8647 61.4242 54.468 61.6065 56.0052ZM67.9845 44.9805C66.5598 45.592 65.0026 46.1375 63.3956 46.6499C62.4513 43.4102 61.2089 40.0383 59.6682 36.5838C61.1923 33.1458 62.4182 29.7904 63.3459 26.5673C64.986 27.0797 66.5432 27.6417 68.0011 28.2533C73.8656 30.7491 77.6592 34.0218 77.6592 36.6169C77.6427 39.2119 73.849 42.5011 67.9845 44.9805Z" fill="#61DAFB"/>
<path d="M40.6832 44.1706C44.8644 44.1706 48.254 40.7886 48.254 36.6169C48.254 32.4451 44.8644 29.0632 40.6832 29.0632C36.502 29.0632 33.1124 32.4451 33.1124 36.6169C33.1124 40.7886 36.502 44.1706 40.6832 44.1706Z" fill="#61DAFB"/>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

+9
View File
@@ -0,0 +1,9 @@
import type { ReactNode } from 'react'
interface RootRouteProps {
children: ReactNode
}
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
return <main className="main">{children}</main>
}
+14
View File
@@ -0,0 +1,14 @@
use serde::Serialize;
use tuono_lib::{Request, Response};
#[derive(Serialize)]
struct MyResponse<'a> {
subtitle: &'a str,
}
#[tuono_lib::handler]
async fn get_server_side_props(_req: Request<'_>, _fetch: reqwest::Client) -> Response {
Response::Props(Box::new(MyResponse {
subtitle: "The react / rust fullstack framework",
}))
}
+47
View File
@@ -0,0 +1,47 @@
import type { TuonoProps } from 'tuono'
interface IndexProps {
subtitle: string
}
export default function IndexPage({
data,
isLoading,
}: TuonoProps<IndexProps>): JSX.Element {
if (isLoading) {
return <h1>Loading...</h1>
}
return (
<>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<div className="subtitle-wrap">
<p className="subtitle">{data?.subtitle}</p>
<a
href="https://github.com/Valerioageno/tuono"
target="_blank"
className="button"
type="button"
>
Github
</a>
</div>
</>
)
}
+109
View File
@@ -0,0 +1,109 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Poppins', sans-serif;
font-weight: 400;
font-style: normal;
}
body {
background: #fbfbfb;
}
main {
width: 673px;
margin: 20px auto;
}
.header {
display: flex;
gap: 20px;
}
.header a {
color: black;
font-size: 18px;
font-weight: 600;
text-decoration: none;
z-index: 2;
}
.title-wrap {
height: 200px;
}
.title {
position: absolute;
font-size: 200px;
line-height: 200px;
z-index: 0;
letter-spacing: -2px;
margin-left: -8px;
user-select: none;
pointer-events: none;
}
.title span {
opacity: 0;
}
.button {
width: 140px;
height: 30px;
border: solid 3px black;
border-radius: 10px;
color: black;
text-decoration: none;
display: flex;
justify-content: center;
align-items: center;
transition: 0.2s;
}
.button:hover {
color: white;
background: black;
}
.logo {
margin-left: 240px;
position: relative;
top: 25px;
}
.logo img {
position: absolute;
}
.rust {
animation: rotate 6s ease-in-out infinite;
}
.react {
top: 33px;
left: 28px;
animation: rotate 6s linear infinite reverse;
}
.subtitle {
font-size: 30px;
line-height: 30px;
letter-spacing: -1px;
}
.subtitle-wrap {
display: flex;
justify-content: space-between;
}
+25
View File
@@ -0,0 +1,25 @@
{
"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 */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
+13
View File
@@ -0,0 +1,13 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.tuono
out
target
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "tuono"
version = "0.0.1"
edition = "2021"
[[bin]]
name = "tuono"
path = ".tuono/main.rs"
[dependencies]
axum = {version = "0.7.5", features = ["json"]}
tokio = { version = "1.37.0", features = ["full"] }
tower-http = {version = "0.5.2", features = ["fs"]}
serde = { version = "1.0.202", features = ["derive"] }
tuono_lib = { path = "../../crates/tuono_lib/"}
serde_json = "1.0"
reqwest = {version = "0.12.4", features = ["json"]}
+3
View File
@@ -0,0 +1,3 @@
# Tuono tutorial
This project is the outcome of the [tuono tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md)
+15
View File
@@ -0,0 +1,15 @@
{
"name": "tuono-tutorial",
"description": "The react/rust fullstack framework",
"version": "0.0.1",
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1",
"tuono": "link:../../packages/tuono"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"typescript": "^5.4.5"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="82" height="73" viewBox="0 0 82 73" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M81.337 36.6169C81.337 31.245 74.5945 26.1541 64.2571 22.9971C66.6426 12.4847 65.5824 4.1211 60.9107 1.44343C59.8339 0.815331 58.5748 0.517811 57.1998 0.517811V4.20375C57.9619 4.20375 58.5748 4.35251 59.0884 4.6335C61.3414 5.92275 62.3188 10.8318 61.5568 17.1458C61.3745 18.6996 61.0763 20.3359 60.7119 22.0053C57.4649 21.212 53.9197 20.6004 50.1923 20.2037C47.9558 17.1459 45.6365 14.369 43.3007 11.9393C48.7013 6.93101 53.7706 4.18722 57.2164 4.18722V0.501282C52.6607 0.501282 46.6967 3.74094 40.6666 9.36076C34.6364 3.774 28.6726 0.567398 24.1168 0.567398V4.25333C27.5461 4.25333 32.6319 6.9806 38.0326 11.9558C35.7133 14.3855 33.394 17.1459 31.1907 20.2037C27.4467 20.6004 23.9015 21.212 20.6545 22.0219C20.2735 20.369 19.9918 18.7657 19.793 17.2285C19.0144 10.9145 19.9752 6.0054 22.2117 4.69962C22.7087 4.4021 23.3548 4.26987 24.1168 4.26987V0.583927C22.7252 0.583927 21.4662 0.881447 20.3728 1.50954C15.7177 4.18722 14.674 12.5343 17.0761 23.0136C6.77188 26.1871 0.0625 31.2615 0.0625 36.6169C0.0625 41.9887 6.805 47.0796 17.1424 50.2366C14.7568 60.749 15.8171 69.1126 20.4888 71.7903C21.5656 72.4184 22.8246 72.7159 24.2162 72.7159C28.772 72.7159 34.7359 69.4762 40.766 63.8564C46.7962 69.4432 52.76 72.6498 57.3158 72.6498C58.7073 72.6498 59.9664 72.3523 61.0598 71.7242C65.7149 69.0465 66.7586 60.6994 64.3565 50.2201C74.6276 47.0631 81.337 41.9722 81.337 36.6169ZM59.7676 25.5921C59.1547 27.7243 58.3926 29.9227 57.5311 32.121C56.8519 30.7987 56.1396 29.4764 55.361 28.1541C54.5989 26.8318 53.7872 25.5425 52.9754 24.2863C55.3278 24.6334 57.5974 25.0632 59.7676 25.5921ZM52.1802 43.1953C50.888 45.4267 49.5628 47.5424 48.1877 49.5094C45.7194 49.7243 43.2178 49.84 40.6997 49.84C38.1982 49.84 35.6967 49.7242 33.2449 49.5259C31.8699 47.559 30.528 45.4598 29.2358 43.2449C27.9768 41.0797 26.8337 38.8813 25.79 36.6664C26.8172 34.4516 27.9768 32.2367 29.2193 30.0714C30.5114 27.84 31.8368 25.7243 33.2118 23.7574C35.6801 23.5425 38.1817 23.4268 40.6997 23.4268C43.2013 23.4268 45.7028 23.5425 48.1546 23.7409C49.5296 25.7078 50.8715 27.807 52.1637 30.0218C53.4227 32.1871 54.5658 34.3855 55.6094 36.6003C54.5658 38.8152 53.4227 41.0301 52.1802 43.1953ZM57.5311 41.0466C58.4257 43.2615 59.1878 45.4763 59.8173 47.6251C57.6471 48.154 55.361 48.6003 52.992 48.9474C53.8037 47.6747 54.6155 46.3689 55.3775 45.0301C56.1396 43.7077 56.8519 42.3689 57.5311 41.0466ZM40.7329 58.6829C39.1922 57.0961 37.6515 55.3275 36.1274 53.3937C37.6184 53.4598 39.1425 53.5094 40.6832 53.5094C42.2404 53.5094 43.7811 53.4763 45.2886 53.3937C43.7976 55.3275 42.257 57.0961 40.7329 58.6829ZM28.4075 48.9474C26.0551 48.6003 23.7855 48.1705 21.6153 47.6416C22.2283 45.5094 22.9903 43.311 23.8518 41.1127C24.531 42.435 25.2434 43.7573 26.022 45.0796C26.8006 46.402 27.5958 47.6912 28.4075 48.9474ZM40.6501 14.5508C42.1907 16.1376 43.7314 17.9062 45.2555 19.8401C43.7645 19.7739 42.2404 19.7244 40.6997 19.7244C39.1425 19.7244 37.6018 19.7574 36.0943 19.8401C37.5853 17.9062 39.126 16.1376 40.6501 14.5508ZM28.391 24.2863C27.5792 25.559 26.7675 26.8648 26.0054 28.2037C25.2433 29.526 24.531 30.8483 23.8518 32.1706C22.9572 29.9557 22.1951 27.7409 21.5656 25.5921C23.7358 25.0797 26.022 24.6334 28.391 24.2863ZM13.3984 44.9805C7.53392 42.4846 3.74023 39.2119 3.74023 36.6169C3.74023 34.0218 7.53392 30.7326 13.3984 28.2533C14.8231 27.6417 16.3803 27.0962 17.9873 26.5838C18.9316 29.8235 20.1741 33.1954 21.7147 36.6499C20.1906 40.0879 18.9647 43.4433 18.037 46.6664C16.3969 46.154 14.8397 45.592 13.3984 44.9805ZM22.3111 68.6002C20.0581 67.311 19.0807 62.4019 19.8427 56.0879C20.0249 54.5341 20.3231 52.8978 20.6876 51.2284C23.9346 52.0218 27.4798 52.6333 31.2072 53.03C33.4437 56.0879 35.763 58.8647 38.0988 61.2945C32.6982 66.3027 27.6289 69.0465 24.1831 69.0465C23.4376 69.03 22.8081 68.8812 22.3111 68.6002ZM61.6065 56.0052C62.3851 62.3192 61.4242 67.2283 59.1878 68.5341C58.6908 68.8316 58.0447 68.9639 57.2827 68.9639C53.8534 68.9639 48.7675 66.2366 43.3669 61.2614C45.6862 58.8317 48.0055 56.0713 50.2088 53.0135C53.9528 52.6168 57.498 52.0052 60.745 51.1953C61.1261 52.8647 61.4242 54.468 61.6065 56.0052ZM67.9845 44.9805C66.5598 45.592 65.0026 46.1375 63.3956 46.6499C62.4513 43.4102 61.2089 40.0383 59.6682 36.5838C61.1923 33.1458 62.4182 29.7904 63.3459 26.5673C64.986 27.0797 66.5432 27.6417 68.0011 28.2533C73.8656 30.7491 77.6592 34.0218 77.6592 36.6169C77.6427 39.2119 73.849 42.5011 67.9845 44.9805Z" fill="#61DAFB"/>
<path d="M40.6832 44.1706C44.8644 44.1706 48.254 40.7886 48.254 36.6169C48.254 32.4451 44.8644 29.0632 40.6832 29.0632C36.502 29.0632 33.1124 32.4451 33.1124 36.6169C33.1124 40.7886 36.502 44.1706 40.6832 44.1706Z" fill="#61DAFB"/>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

@@ -0,0 +1,26 @@
.link {
width: 100%;
max-width: 216px;
position: relative;
background: white;
margin-bottom: 10px;
border: solid #f0f0f0 1px;
text-decoration: none;
color: black;
padding: 5px 5px 5px 15px;
border-radius: 10px;
display: flex;
justify-content: space-between;
transition: 0.2s;
align-items: center;
}
.link:hover {
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
}
.link img {
width: 70px;
background: white;
border-radius: 50%;
}
@@ -0,0 +1,23 @@
import { Link } from 'tuono'
import styles from './PokemonLink.module.css'
interface Pokemon {
name: string
}
export default function PokemonLink({
pokemon,
id,
}: {
pokemon: Pokemon
id: number
}): JSX.Element {
return (
<Link className={styles.link} href={`/pokemons/${pokemon.name}`}>
{pokemon.name}
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
/>
</Link>
)
}
@@ -0,0 +1,38 @@
.back-btn {
background-color: white;
border-radius: 10px;
padding: 7px 15px;
color: black;
text-decoration: none;
border: solid #f0f0f0 1px;
font-size: 20px;
}
.back-btn:hover {
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
}
.pokemon {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.name {
font-size: 50px;
font-weight: 700;
}
.pokemon img {
width: 400px;
}
.spec {
display: flex;
font-size: 18px;
margin-top: 10px;
}
.label {
font-weight: 700;
}
@@ -0,0 +1,41 @@
import { Link } from 'tuono'
import styles from './PokemonView.module.css'
interface Pokemon {
name: string
id: string
weight: number
height: number
}
export default function PokemonView({
pokemon,
}: {
pokemon?: Pokemon
}): JSX.Element {
return (
<div>
<Link className={styles['back-btn']} href="/">
Back
</Link>
{pokemon?.name && (
<div className={styles.pokemon}>
<div>
<h1 className={styles.name}>{pokemon.name}</h1>
<dl className={styles.spec}>
<dt className={styles.label}>Weight:</dt>
<dd>{pokemon.weight}lbs</dd>
</dl>
<dl className={styles.spec}>
<dt className={styles.label}>Height:</dt>
<dd>{pokemon.height}ft</dd>
</dl>
</div>
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${pokemon.id}.png`}
/>
</div>
)}
</div>
)
}
+9
View File
@@ -0,0 +1,9 @@
import type { ReactNode } from 'react'
interface RootRouteProps {
children: ReactNode
}
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
return <main className="main">{children}</main>
}
+27
View File
@@ -0,0 +1,27 @@
// src/routes/index.rs
use serde::{Deserialize, Serialize};
use tuono_lib::{Request, Response};
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
#[derive(Debug, Serialize, Deserialize)]
struct Pokemons {
results: Vec<Pokemon>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Pokemon {
name: String,
url: String,
}
#[tuono_lib::handler]
async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response {
return match fetch.get(ALL_POKEMON).send().await {
Ok(res) => {
let data = res.json::<Pokemons>().await.unwrap();
Response::Props(Box::new(data))
}
Err(_err) => Response::Props(Box::new(Pokemons { results: vec![] })),
};
}
+46
View File
@@ -0,0 +1,46 @@
// src/routes/index.tsx
import type { TuonoProps } from 'tuono'
import PokemonLink from '../components/PokemonLink'
interface Pokemon {
name: string
}
interface IndexProps {
results: Pokemon[]
}
export default function IndexPage({
data,
}: TuonoProps<IndexProps>): JSX.Element {
if (!data?.results) {
return <></>
}
return (
<>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<ul style={{ flexWrap: 'wrap', display: 'flex', gap: 10 }}>
{data.results.map((pokemon, i) => {
return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />
})}
</ul>
</>
)
}
@@ -0,0 +1,31 @@
use serde::{Deserialize, Serialize};
use tuono_lib::{Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
#[derive(Debug, Serialize, Deserialize)]
struct Pokemon {
name: String,
id: u16,
weight: u16,
height: u16,
}
#[tuono_lib::handler]
async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response {
// The param `pokemon` is defined in the route filename [pokemon].rs
let pokemon = req.params.get("pokemon").unwrap();
return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
Ok(res) => {
let data = res.json::<Pokemon>().await.unwrap();
Response::Props(Box::new(data))
}
Err(_err) => Response::Props(Box::new(Pokemon {
name: "Nope".to_string(),
id: 0,
weight: 0,
height: 0,
})),
};
}
@@ -0,0 +1,6 @@
import { TuonoProps } from 'tuono'
import PokemonView from '../../components/PokemonView'
export default function Pokemon({ data }: TuonoProps): JSX.Element {
return <PokemonView pokemon={data} />
}
+109
View File
@@ -0,0 +1,109 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Poppins', sans-serif;
font-weight: 400;
font-style: normal;
}
body {
background: #fbfbfb;
}
main {
width: 673px;
margin: 20px auto;
}
.header {
display: flex;
gap: 20px;
}
.header a {
color: black;
font-size: 18px;
font-weight: 600;
text-decoration: none;
z-index: 2;
}
.title-wrap {
height: 200px;
}
.title {
position: absolute;
font-size: 200px;
line-height: 200px;
z-index: 0;
letter-spacing: -2px;
margin-left: -8px;
user-select: none;
pointer-events: none;
}
.title span {
opacity: 0;
}
.button {
width: 140px;
height: 30px;
border: solid 3px black;
border-radius: 10px;
color: black;
text-decoration: none;
display: flex;
justify-content: center;
align-items: center;
transition: 0.2s;
}
.button:hover {
color: white;
background: black;
}
.logo {
margin-left: 240px;
position: relative;
top: 25px;
}
.logo img {
position: absolute;
}
.rust {
animation: rotate 6s ease-in-out infinite;
}
.react {
top: 33px;
left: 28px;
animation: rotate 6s linear infinite reverse;
}
.subtitle {
font-size: 30px;
line-height: 30px;
letter-spacing: -1px;
}
.subtitle-wrap {
display: flex;
justify-content: space-between;
}
+25
View File
@@ -0,0 +1,25 @@
{
"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 */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
+5 -2
View File
@@ -1,9 +1,9 @@
{
"name": "tuono",
"version": "0.1.0",
"version": "0.1.2",
"description": "",
"main": "src/index.js",
"packageManager": "pnpm@8.12.1",
"packageManager": "pnpm@9.1.1",
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
@@ -32,5 +32,8 @@
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vitest": "^1.5.2"
},
"dependencies": {
"turbo": "^2.0.3"
}
}
-6
View File
@@ -1,6 +0,0 @@
# Tuono packages
- `tuono-vite`: Handles the bundling (with chunk splitting)
- `tuono-routes-generator`: Handles the creation of the `routeGen.tree.ts` file
- `tuono-router`: Exposes all the Functions and Components related to the routing
- `tuono`: Collects and exposes all the public APIs. It's the only published package
-44
View File
@@ -1,44 +0,0 @@
{
"name": "tuono-router",
"version": "0.0.1",
"description": "",
"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"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
"format": "prettier -u --write '**/*'",
"test": "vitest --watch=false",
"types": "tsc --noEmit"
},
"peerDependencies": {
"react": ">=16.3.0",
"react-dom": ">=16.3.0"
},
"type": "module",
"types": "dist/esm/index.d.ts",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"jsdom": "^24.0.0"
},
"dependencies": {
"zustand": "4.4.7"
}
}
@@ -1,22 +0,0 @@
import { useRouter } from '../hooks/useRouter'
import { useRouterStore } from '../hooks/useRouterStore'
import { RouteMatch } from './RouteMatch'
import NotFound from './NotFound'
interface MatchesProps {
// user defined props
serverSideProps: any
}
export function Matches({ serverSideProps }: MatchesProps): JSX.Element {
const location = useRouterStore((st) => st.location)
const router = useRouter()
const route = router.routesById[location.pathname]
if (!route) {
return <NotFound />
}
return <RouteMatch route={route} serverSideProps={serverSideProps} />
}
@@ -1,34 +0,0 @@
import { expectTypeOf, test } from 'vitest'
import {
RouterProvider,
createRootRoute,
createRoute,
createRouter,
} from '../../src'
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
})
const invoicesRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/invoices',
})
const routeTree = rootRoute.addChildren([invoicesRoute, indexRoute])
const defaultRouter = createRouter({
routeTree,
})
type DefaultRouter = typeof defaultRouter
test('can pass default router to the provider', () => {
expectTypeOf(RouterProvider).parameter(0).toMatchTypeOf<{
router: DefaultRouter
routeTree?: DefaultRouter['routeTree']
}>()
})
-28
View File
@@ -1,28 +0,0 @@
/* eslint-disable */
import { describe, it, expect } from 'vitest'
import { getRouteApi, createRoute } from '../src'
describe('getRouteApi', () => {
it('should have the useRouteContext hook', () => {
const api = getRouteApi('foo')
expect(api.useRouteContext).toBeDefined()
})
it('should have the useParams hook', () => {
const api = getRouteApi('foo')
expect(api.useParams).toBeDefined()
})
})
describe('createRoute has the same hooks as getRouteApi', () => {
const routeApi = getRouteApi('foo')
const hookNames = Object.keys(routeApi).filter((key) => key.startsWith('use'))
const route = createRoute({} as any)
it.each(hookNames.map((name) => [name]))(
'should have the "%s" hook defined',
(hookName) => {
expect(route[hookName as keyof typeof route]).toBeDefined()
},
)
})
-7
View File
@@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
},
"include": ["src", "tests", "vite.config.ts"],
}
-20
View File
@@ -1,20 +0,0 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackBuildConfig } from '@tanstack/config/build'
import react from '@vitejs/plugin-react'
const config = defineConfig({
plugins: [react()],
test: {
name: 'react-router',
watch: false,
environment: 'jsdom',
},
})
export default mergeConfig(
config,
tanstackBuildConfig({
entry: './src/index.tsx',
srcDir: './src',
}),
)
@@ -1 +0,0 @@
dist/
-48
View File
@@ -1,48 +0,0 @@
# Router generator
This package handle the creation of the routes.
Basically collects and manages them in a single entry point file.
## Generated route file
Currently the generator file is very similar to `@tanstack/router` one but since it does not need the same
configurability is up to strong updates.
```tsx
// This file is auto-generated by Tuono
import { Route as rootRoute } from './routes/__root'
import { Route as AboutImport } from './routes/about'
import { Route as IndexImport } from './routes/index'
// Create/Update Routes
const AboutRoute = AboutImport.update({
path: '/about',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = IndexImport.update({
path: '/',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/about': {
preLoaderRoute: typeof AboutImport
parentRoute: typeof rootRoute
}
}
}
// Create and export the route tree
export const routeTree = rootRoute.addChildren([IndexRoute, AboutRoute])
```
@@ -1,41 +0,0 @@
{
"name": "tuono-routes-generator",
"version": "0.1.0",
"description": "",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
"format": "prettier -u --write '**/*'",
"test": "vitest --watch=false",
"types": "tsc --noEmit"
},
"keywords": [],
"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"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
"files": [
"dist",
"src"
],
"dependencies": {
"prettier": "^3.2.4"
},
"type": "module",
"types": "dist/esm/index.d.ts",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"author": "Valerio Ageno",
"license": "MIT"
}
@@ -1,41 +0,0 @@
import fs from 'fs/promises'
import { describe, it, expect } from 'vitest'
import { routeGenerator } from '../src'
function makeFolderDir(folder: string) {
return process.cwd() + `/tests/generator/${folder}`
}
async function getRouteTreeFileText(folder: string) {
const dir = makeFolderDir(folder)
return await fs.readFile(dir + '/routeTree.gen.ts', 'utf-8')
}
async function getExpectedRouteTreeFileText(folder: string) {
const dir = makeFolderDir(folder)
const location = dir + '/routeTree.expected.ts'
return await fs.readFile(location, 'utf-8')
}
describe('generator works', async () => {
const folderNames = await fs.readdir(process.cwd() + '/tests/generator')
it.each(folderNames.map((folder) => [folder]))(
'should wire-up the routes for a "%s" tree',
async (folderName) => {
const currentFolder = `${process.cwd()}/tests/generator/${folderName}`
await routeGenerator({
folderName: `${currentFolder}/routes`,
generatedRouteTree: `${currentFolder}/routeTree.gen.ts`,
})
const [expectedRouteTree, generatedRouteTree] = await Promise.all([
getExpectedRouteTreeFileText(folderName),
getRouteTreeFileText(folderName),
])
expect(generatedRouteTree).equal(expectedRouteTree)
},
)
})
@@ -1,48 +0,0 @@
// This file is auto-generated by Tuono
import { createRoute } from 'tuono'
import RootImport from './routes/__root'
import AboutImport from './routes/about'
import IndexImport from './routes/index'
import PostsIndexImport from './routes/posts/index'
import PostsPostSlugImport from './routes/posts/post-slug'
const rootRoute = createRoute({ isRoot: true, component: RootImport })
const About = createRoute({ component: AboutImport })
const Index = createRoute({ component: IndexImport })
const PostsIndex = createRoute({ component: PostsIndexImport })
const PostsPostSlug = createRoute({ component: PostsPostSlugImport })
// Create/Update Routes
const AboutRoute = About.update({
path: '/about',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = Index.update({
path: '/',
getParentRoute: () => rootRoute,
} as any)
const PostsIndexRoute = PostsIndex.update({
path: '/posts/',
getParentRoute: () => rootRoute,
} as any)
const PostsPostSlugRoute = PostsPostSlug.update({
path: '/posts/post-slug',
getParentRoute: () => rootRoute,
} as any)
// Create and export the route tree
export const routeTree = rootRoute.addChildren([
IndexRoute,
AboutRoute,
PostsPostSlugRoute,
PostsIndexRoute,
])
@@ -1,5 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: () => <div>Hello /about!</div>,
})
@@ -1,29 +0,0 @@
// This file is auto-generated by Tuono
import { createRoute } from 'tuono'
import RootImport from './routes/__root'
import AboutImport from './routes/about'
import IndexImport from './routes/index'
const rootRoute = createRoute({ isRoot: true, component: RootImport })
const About = createRoute({ component: AboutImport })
const Index = createRoute({ component: IndexImport })
// Create/Update Routes
const AboutRoute = About.update({
path: '/about',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = Index.update({
path: '/',
getParentRoute: () => rootRoute,
} as any)
// Create and export the route tree
export const routeTree = rootRoute.addChildren([IndexRoute, AboutRoute])
@@ -1,30 +0,0 @@
// This file is auto-generated by Tuono
import { createRoute } from 'tuono'
import RootImport from './routes/__root'
import AboutImport from './routes/about'
import IndexImport from './routes/index'
const rootRoute = createRoute({ isRoot: true, component: RootImport })
const About = createRoute({ component: AboutImport })
const Index = createRoute({ component: IndexImport })
// Create/Update Routes
const AboutRoute = About.update({
path: '/about',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = Index.update({
path: '/',
getParentRoute: () => rootRoute,
hasHandler: true,
} as any)
// Create and export the route tree
export const routeTree = rootRoute.addChildren([IndexRoute, AboutRoute])
@@ -1,4 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "vite.config.ts"]
}
@@ -1,12 +0,0 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackBuildConfig } from '@tanstack/config/build'
const config = defineConfig({})
export default mergeConfig(
config,
tanstackBuildConfig({
entry: './src/index.ts',
srcDir: './src',
}),
)
-2
View File
@@ -1,2 +0,0 @@
dist/
tests/snapshots
-5
View File
@@ -1,5 +0,0 @@
# [Tuono] Vite FS Router
> Strongly inspired by [@tanstack/router/router-vite-plugin](https://github.com/TanStack/router/tree/main/packages/router-vite-plugin)
This is the [vite](https://vitejs.dev/) plugin that automatically handles the file system routing.
-55
View File
@@ -1,55 +0,0 @@
{
"name": "tuono-vite",
"version": "0.0.1",
"description": "",
"main": "dist/cjs/index.cjs",
"types": "dist/esm/index.d.ts",
"module": "dist/esm/index.js",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
"format": "prettier -u --write '**/*'",
"test": "vitest --typecheck --watch=false",
"types": "tsc --noEmit"
},
"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"
}
},
"./package.json": "./package.json"
},
"files": [
"dist",
"src/**"
],
"sideEffects": false,
"keywords": [],
"type": "module",
"author": "Valerio Ageno",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/generator": "^7.23.6",
"@babel/plugin-syntax-jsx": "^7.24.1",
"@babel/plugin-syntax-typescript": "^7.24.1",
"@babel/plugin-transform-react-jsx": "^7.23.4",
"@babel/plugin-transform-typescript": "^7.24.1",
"@babel/template": "^7.24.0",
"@babel/traverse": "^7.24.1",
"@babel/types": "^7.24.0",
"@types/babel__core": "^7.20.5",
"@types/node": "^20.12.7",
"tuono-routes-generator": "workspace:*"
},
"devDependencies": {
"@types/babel-traverse": "^6.25.10"
}
}
-33
View File
@@ -1,33 +0,0 @@
import { readFile, readdir } from 'fs/promises'
import path from 'path'
import { expect, test } from 'vitest'
import { makeCompile, splitFile } from '../src/compiler'
import { SPLIT_PREFIX } from '../src/constants'
async function splitTestFile(opts: { file: string }): Promise<void> {
const code = (
await readFile(path.resolve(__dirname, `./test-files/${opts.file}`))
).toString()
const filename = opts.file.replace(__dirname, '')
const result = await splitFile({
code,
compile: makeCompile({
root: './test-files',
}),
filename: `${filename}?${SPLIT_PREFIX}`,
})
await expect(result.code).toMatchFileSnapshot(
`./snapshots/${filename.replace('.tsx', '')}?split.tsx`,
)
}
test('it compiles and splits', async (): Promise<void> => {
// get the list of files from the /test-files directory
const files = await readdir(path.resolve(__dirname, './test-files'))
for (const file of files) {
await splitTestFile({ file })
}
})
@@ -1,3 +0,0 @@
export const ImportedRoute = (): JSX.Element => {
return <h1>Imported route</h1>
}
@@ -1,3 +0,0 @@
export default function Route(): JSX.Element {
return <h1>Test route</h1>;
}
@@ -1,4 +0,0 @@
function PostsRoute() {
return <h1>Post route</h1>;
}
export default PostsRoute;
@@ -1,2 +0,0 @@
import { ImportedRoute } from '../shared/imported';
export default ImportedRoute;
@@ -1,5 +0,0 @@
import * as React from 'react'
export default function Route(): JSX.Element {
return <h1>Test route</h1>
}
@@ -1,7 +0,0 @@
import * as React from 'react'
function PostsRoute() {
return <h1>Post route</h1>
}
export default PostsRoute
@@ -1,4 +0,0 @@
import * as React from 'react'
import { ImportedRoute } from '../shared/imported'
export default ImportedRoute
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "vite.config.ts", "tests"],
"exclude": ["tests/test-files/**", "tests/snapshots/**"],
"compilerOptions": {
"jsx": "react"
}
}
-13
View File
@@ -1,13 +0,0 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackBuildConfig } from '@tanstack/config/build'
const config = defineConfig({})
export default mergeConfig(
config,
tanstackBuildConfig({
entry: './src/index.ts',
srcDir: './src',
exclude: ['./src/tests/'],
}),
)
+50 -7
View File
@@ -1,14 +1,14 @@
{
"name": "tuono",
"version": "0.0.4",
"description": "",
"version": "0.1.2",
"description": "The react/rust fullstack framework",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
"format": "prettier -u --write '**/*'",
"types": "tsc --noEmit",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "vitest"
},
"type": "module",
"types": "dist/esm/index.d.ts",
@@ -25,6 +25,26 @@
"default": "./dist/cjs/build/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"
}
},
"./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"
}
},
".": {
"import": {
"types": "./dist/esm/index.d.ts",
@@ -45,14 +65,37 @@
"files": [
"dist",
"src",
"../../README.md",
"README.md",
"bin/**"
],
"peerDependencies": {
"react": ">=16.3.0",
"react-dom": ">=16.3.0"
},
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/generator": "^7.23.6",
"@babel/plugin-syntax-jsx": "^7.24.1",
"@babel/plugin-syntax-typescript": "^7.24.1",
"@babel/plugin-transform-react-jsx": "^7.23.4",
"@babel/plugin-transform-typescript": "^7.24.1",
"@babel/template": "^7.24.0",
"@babel/traverse": "^7.24.1",
"@babel/types": "^7.24.0",
"@types/babel__core": "^7.20.5",
"@types/node": "^20.12.7",
"@vitejs/plugin-react-swc": "^3.7.0",
"tuono-router": "workspace:*",
"tuono-vite": "workspace:*",
"vite": "^5.2.11"
"fast-text-encoding": "^1.0.6",
"prettier": "^3.2.4",
"vite": "^5.2.11",
"zustand": "4.4.7"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/babel-traverse": "^6.25.10",
"jsdom": "^24.0.0",
"vitest": "^1.5.2"
},
"sideEffects": false,
"keywords": [],
+6 -6
View File
@@ -1,10 +1,10 @@
import { build, createServer } from 'vite'
import { build, createServer, InlineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { ViteFsRouter } from 'tuono-vite'
import { ViteFsRouter } from './tuono-vite-plugin'
const BASE_CONFIG = {
silent: true,
const BASE_CONFIG: InlineConfig = {
root: '.tuono',
logLevel: 'silent',
publicDir: '../public',
cacheDir: 'cache',
envDir: '../',
@@ -13,7 +13,6 @@ const BASE_CONFIG = {
export function developmentSSRBundle() {
;(async () => {
console.log('Build SSR')
await build({
...BASE_CONFIG,
build: {
@@ -23,6 +22,8 @@ export function developmentSSRBundle() {
emptyOutDir: true,
rollupOptions: {
input: './.tuono/server-main.tsx',
// Silent all logs
onLog() {},
output: {
entryFileNames: 'dev-server.js',
format: 'iife',
@@ -39,7 +40,6 @@ export function developmentSSRBundle() {
export function developmentCSRWatch() {
;(async () => {
console.log('Watch files')
const server = await createServer({
...BASE_CONFIG,
server: {
@@ -134,12 +134,9 @@ export function hasParentRoute(
export async function routeGenerator(config = defaultConfig): Promise<void> {
if (!isFirst) {
console.log('Generating routes...')
isFirst = true
} else if (skipMessage) {
skipMessage = false
} else {
console.log('Regenerating routes')
}
const checkLatest = (): boolean => {
@@ -190,33 +187,10 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
removeUnderscores(removeLayoutSegments(node.path)) ?? '',
)
const routeCode = fs.readFileSync(node.fullPath, 'utf-8')
const escapedRoutePath = removeTrailingUnderscores(
node.routePath.replaceAll('$', '$$'),
)
let replaced = routeCode
if (!routeCode) {
replaced = [
`import { createFileRoute } from '@tanstack/react-router'`,
`export const Route = createFileRoute('${escapedRoutePath}')({
component: () => <div>Hello ${escapedRoutePath}!</div>
})`,
].join('\n\n')
if (replaced !== routeCode) {
console.log(`[emoticon] Updating ${node.fullPath}`)
await fsp.writeFile(node.fullPath, replaced)
}
}
if (node.parent) {
node.parent.children = node.parent.children ?? []
node.parent.children.push(node)
}
console.log('pushed')
routeNodes.push(node)
}
@@ -371,10 +345,4 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
routeConfigFileContent,
)
}
console.log(
`[emoticon] Processed ${routeNodes.length === 1 ? 'route' : 'routes'} in ${
Date.now() - start
}ms`,
)
}
@@ -100,7 +100,6 @@ export const splitFile = async ({
compile,
filename,
}: SplitFileFnArgs): Promise<CompileOutput> => {
console.log('Split file [splitFile fn]', code)
return compile({
code,
filename,
@@ -220,7 +219,6 @@ export const splitFile = async ({
]),
)
} else {
console.log(splitNode)
throw new Error(
`Unexpected splitNode type ${splitNode.type}`,
)

Some files were not shown because too many files have changed in this diff Show More