Optimize build assets (#6)

* feat: handle Dev and Prod modes

* fix: remove useles variable override

* refactor: ssr entry point fn

* feat: pass Mode from server to client

* feat: enable read file from out/client dir

* feat: load manifest data from server side

* feat: remap manifest when loaded

* feat: load correct bundle server side

* doc: update README.md

* doc: update tutorial
This commit is contained in:
Valerio Ageno
2024-06-23 20:15:33 +02:00
committed by GitHub
parent 4ff899729b
commit 9c707466db
16 changed files with 596 additions and 83 deletions
+12 -9
View File
@@ -30,8 +30,8 @@ React is still React - it is just superpowered.
**Rust is an hard language then writing server side code is hard as well right? (no again)**
Tuono provides a collection of utilities to handle the server side code seamlessly with the React code.
Each server side route is managed with a separate file alongside the React route (the client/server
logic matching is handled by Tuono itself).
Each server side route is managed with a separate file alongside the React route. The routing is handled
by Tuono based on the files defined within the `./src/routes` directory.
## Features
@@ -39,18 +39,18 @@ logic matching is handled by Tuono itself).
- 🌐 Routing
- 🔥 Hot Module Reload
- 🍭 CSS modules
- 📨 Server Side Rendering
- 🌐 Multi thread backend
- Create custom APIs*
- 🧬 Server Side Rendering
- 🧵 Multi thread backend
- ⚙️ Build optimizations
- Custom APIs*
- Image optimization*
- Build optimization*
- Server streamed content*
> *Development in progress
## Getting started
As already mentioned above I strongly suggest to take a look to the
As already mentioned above I strongly suggest you to take a look at the
[tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
Tuono is basically a CLI that provides all the commands to handle the fullstack project.
@@ -64,7 +64,10 @@ To create a new project run `tuono new [NAME]` (optionally you can pass the `--t
Then to run the local development environment run inside the project folder `tuono dev`
> 💡 The `tuono build` command is not ready yet - I'm working on it right now.
Finally when the project will be ready to be deployed just run `tuono build` to create the final React assets
and to set the server project to the `production` mode.
Now to execute it just run `cargo run --release`.
## Requirements
@@ -87,7 +90,7 @@ Then to run the local development environment run inside the project folder `tuo
```
## Contributing
Any help or suggestion will be appreciated.
Any help or suggestion will be appreciated and encouraged.
## License
+6 -5
View File
@@ -4,7 +4,8 @@ use std::process::Command;
mod source_builder;
use source_builder::{bundle_axum_source, create_client_entry_files};
use crate::source_builder::check_tuono_folder;
use crate::source_builder::{check_tuono_folder, Mode};
mod scaffold_project;
mod watch;
@@ -32,9 +33,9 @@ struct Args {
action: Actions,
}
fn init_tuono_folder() -> std::io::Result<()> {
fn init_tuono_folder(mode: Mode) -> std::io::Result<()> {
check_tuono_folder()?;
bundle_axum_source()?;
bundle_axum_source(mode)?;
create_client_entry_files()?;
Ok(())
@@ -45,11 +46,11 @@ pub fn cli() -> std::io::Result<()> {
match args.action {
Actions::Dev => {
init_tuono_folder()?;
init_tuono_folder(Mode::Dev)?;
watch::watch().unwrap();
}
Actions::Build => {
init_tuono_folder()?;
init_tuono_folder(Mode::Prod)?;
let mut vite_build = Command::new("./node_modules/.bin/tuono-build-prod");
let _ = &vite_build.output()?;
}
+67 -23
View File
@@ -8,6 +8,21 @@ use std::io::prelude::*;
use std::path::Path;
use std::path::PathBuf;
#[derive(PartialEq, Eq)]
pub enum Mode {
Prod,
Dev,
}
impl Mode {
pub fn as_str<'a>(&self) -> &'a str {
if *self == Mode::Dev {
return "Mode::Dev";
}
"Mode::Prod"
}
}
pub const SERVER_ENTRY_DATA: &str = "// File automatically generated by tuono
// Do not manually update this file
import { routeTree } from './routeTree.gen'
@@ -37,9 +52,11 @@ use axum::response::Html;
use axum::{routing::get, Router};
use tower_http::services::ServeDir;
use std::collections::HashMap;
use tuono_lib::{ssr, Ssr};
use tuono_lib::{ssr, Ssr, Mode, GLOBAL_MODE, manifest::load_manifest};
use reqwest::Client;
const MODE: Mode = /*MODE*/;
// MODULE_IMPORTS
#[tokio::main]
@@ -48,9 +65,18 @@ async fn main() {
let fetch = Client::new();
GLOBAL_MODE.set(MODE).unwrap();
if MODE == Mode::Prod {
load_manifest()
}
let app = Router::new()
// ROUTE_BUILDER
.fallback_service(ServeDir::new("public").fallback(get(catch_all)))
.fallback_service(
ServeDir::new("public"))
.fallback_service(ServeDir::new("out/client")
.fallback(get(catch_all)))
.with_state(fetch);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
@@ -83,11 +109,6 @@ async fn catch_all(Path(params): Path<HashMap<String, String>>, request: Request
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 /
@@ -137,17 +158,15 @@ impl Route {
struct SourceBuilder {
route_map: HashMap<PathBuf, Route>,
mode: Mode,
base_path: PathBuf,
}
impl SourceBuilder {
pub fn new(mode: Mode) -> Self {
pub fn new() -> Self {
let base_path = std::env::current_dir().unwrap();
SourceBuilder {
route_map: HashMap::new(),
mode,
base_path,
}
}
@@ -217,14 +236,22 @@ mod {module_name};
route_declarations
}
pub fn bundle_axum_source() -> io::Result<()> {
pub fn bundle_axum_source(mode: Mode) -> io::Result<()> {
let base_path = std::env::current_dir().unwrap();
let mut source_builder = SourceBuilder::new(Mode::Dev);
let mut source_builder = SourceBuilder::new();
source_builder.collect_routes();
let bundled_file = AXUM_ENTRY_POINT
let bundled_file = generate_axum_source(&source_builder, mode);
create_main_file(&base_path, &bundled_file);
Ok(())
}
fn generate_axum_source(source_builder: &SourceBuilder, mode: Mode) -> String {
AXUM_ENTRY_POINT
.replace(
"// ROUTE_BUILDER\n",
&create_routes_declaration(&source_builder.route_map),
@@ -232,11 +259,8 @@ pub fn bundle_axum_source() -> io::Result<()> {
.replace(
"// MODULE_IMPORTS\n",
&create_modules_declaration(&source_builder.route_map),
);
create_main_file(&base_path, &bundled_file);
Ok(())
)
.replace("/*MODE*/", mode.as_str())
}
pub fn check_tuono_folder() -> io::Result<()> {
@@ -266,7 +290,7 @@ mod tests {
use super::*;
#[test]
fn find_dynamic_paths() {
fn should_find_dynamic_paths() {
let routes = [
("/home/user/Documents/tuono/src/routes/about.rs", false),
("/home/user/Documents/tuono/src/routes/index.rs", false),
@@ -286,8 +310,8 @@ mod tests {
}
#[test]
fn collect_routes() {
let mut source_builder = SourceBuilder::new(Mode::Dev);
fn should_collect_routes() {
let mut source_builder = SourceBuilder::new();
source_builder.base_path = "/home/user/Documents/tuono".into();
let routes = [
@@ -321,8 +345,8 @@ mod tests {
}
#[test]
fn create_multi_level_axum_paths() {
let mut source_builder = SourceBuilder::new(Mode::Dev);
fn should_create_multi_level_axum_paths() {
let mut source_builder = SourceBuilder::new();
source_builder.base_path = "/home/user/Documents/tuono".into();
let routes = [
@@ -356,4 +380,24 @@ mod tests {
)
})
}
#[test]
fn should_set_the_correct_mode() {
let source_builder = SourceBuilder::new();
let dev_bundle = generate_axum_source(&source_builder, Mode::Dev);
assert!(dev_bundle.contains("const MODE: Mode = Mode::Dev;"));
let prod_bundle = generate_axum_source(&source_builder, Mode::Prod);
assert!(prod_bundle.contains("const MODE: Mode = Mode::Prod;"));
}
#[test]
fn should_correctly_print_the_mode_as_str() {
let dev = Mode::Dev.as_str();
let prod = Mode::Prod.as_str();
assert_eq!(dev, "Mode::Dev");
assert_eq!(prod, "Mode::Prod");
}
}
+2 -3
View File
@@ -6,7 +6,7 @@ use watchexec::Watchexec;
use watchexec_signals::Signal;
use watchexec_supervisor::job::{start_job, Job};
use crate::source_builder::bundle_axum_source;
use crate::source_builder::{bundle_axum_source, Mode};
fn watch_react_src() -> Job {
start_job(Arc::new(Command {
@@ -75,10 +75,9 @@ pub async fn watch() -> Result<()> {
run_server.stop();
build_ssr_bundle.stop();
build_ssr_bundle.start();
bundle_axum_source().expect("Failed to bunlde rust source");
bundle_axum_source(Mode::Dev).expect("Failed to bunlde rust source");
run_server.start();
println!("Ready!");
should_reload = false;
}
// if Ctrl-C is received, quit
+3
View File
@@ -25,4 +25,7 @@ erased-serde = "0.4.5"
serde_json = "1.0"
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.1.9"}
once_cell = "1.19.0"
lazy_static = "1.5.0"
regex = "1.10.5"
+3
View File
@@ -1,9 +1,12 @@
pub mod manifest;
mod mode;
mod payload;
mod request;
mod response;
pub mod ssr;
pub use mode::{Mode, GLOBAL_MODE};
pub use payload::Payload;
pub use request::Request;
pub use response::{Props, Response};
+99
View File
@@ -0,0 +1,99 @@
use once_cell::sync::OnceCell;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
const VITE_MANIFEST_PATH: &str = "./out/client/.vite/manifest.json";
#[derive(Deserialize, Debug, Clone)]
pub struct BundleInfo {
pub file: String,
pub css: Vec<String>,
}
/// Manifest is the mapping between the vite output bundled files
/// and the originals.
/// Vite doc: https://vitejs.dev/config/build-options.html#build-manifest
pub static MANIFEST: OnceCell<HashMap<String, BundleInfo>> = OnceCell::new();
pub fn load_manifest() {
let file = File::open(PathBuf::from(VITE_MANIFEST_PATH)).unwrap();
let reader = BufReader::new(file);
let manifest = serde_json::from_reader(reader).unwrap();
MANIFEST.set(remap_manifest_keys(manifest)).unwrap();
}
fn remap_manifest_keys(manifest: HashMap<String, BundleInfo>) -> HashMap<String, BundleInfo> {
let mut new_hashmap = HashMap::new();
manifest.keys().for_each(|key| {
let new_key = key
.replace("../src/routes", "")
.replace(".tsx", "")
.replace(".jsx", "")
.replace("index", "");
new_hashmap.insert(new_key, manifest.get(key).unwrap().clone());
});
new_hashmap
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_correctly_remap_the_manifest() {
let mut parsed_manifest: HashMap<String, BundleInfo> = HashMap::new();
parsed_manifest.insert(
"../src/routes/index.tsx".to_string(),
BundleInfo {
file: "assets/index.js".to_string(),
css: vec!["assets/index.css".to_string()],
},
);
parsed_manifest.insert(
"../src/routes/about.jsx".to_string(),
BundleInfo {
file: "assets/about.js".to_string(),
css: vec!["assets/about.css".to_string()],
},
);
parsed_manifest.insert(
"../src/routes/posts/[post].tsx".to_string(),
BundleInfo {
file: "assets/posts/[post].js".to_string(),
css: vec!["assets/posts/[post].css".to_string()],
},
);
parsed_manifest.insert(
"client-main.tsx".to_string(),
BundleInfo {
file: "assets/main.js".to_string(),
css: vec!["assets/main.css".to_string()],
},
);
let remapped = remap_manifest_keys(parsed_manifest);
assert_eq!(
remapped.get("/").unwrap().file,
"assets/index.js".to_string()
);
assert_eq!(
remapped.get("/about").unwrap().file,
"assets/about.js".to_string()
);
assert_eq!(
remapped.get("/posts/[post]").unwrap().file,
"assets/posts/[post].js".to_string()
);
assert_eq!(
remapped.get("client-main").unwrap().file,
"assets/main.js".to_string()
);
}
}
+10
View File
@@ -0,0 +1,10 @@
use once_cell::sync::OnceCell;
use serde::Serialize;
#[derive(Debug, PartialEq, Eq, Serialize, Clone, Copy)]
pub enum Mode {
Dev,
Prod,
}
pub static GLOBAL_MODE: OnceCell<Mode> = OnceCell::new();
+273 -5
View File
@@ -1,14 +1,25 @@
use crate::{manifest::MANIFEST, Mode, GLOBAL_MODE};
use erased_serde::Serialize;
use regex::Regex;
use serde::Serialize as SerdeSerialize;
use crate::request::Location;
use crate::Request;
use crate::request::{Location, Request};
fn has_dynamic_path(route: &str) -> bool {
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
regex.is_match(route)
}
#[derive(SerdeSerialize)]
/// This is the data shared to the client
/// This is the payload sent to the client for hydration
pub struct Payload<'a> {
router: Location<'a>,
router: Location,
props: &'a dyn Serialize,
mode: Mode,
#[serde(rename(serialize = "jsBundles"))]
js_bundles: Option<Vec<&'a String>>,
#[serde(rename(serialize = "cssBundles"))]
css_bundles: Option<Vec<&'a String>>,
}
impl<'a> Payload<'a> {
@@ -16,10 +27,267 @@ impl<'a> Payload<'a> {
Payload {
router: req.location(),
props,
mode: *GLOBAL_MODE.get().expect("Failed to load the current mode"),
js_bundles: None,
css_bundles: None,
}
}
pub fn client_payload(&self) -> Result<String, serde_json::Error> {
pub fn client_payload(&mut self) -> Result<String, serde_json::Error> {
if self.mode == Mode::Prod {
self.add_bundle_sources();
}
serde_json::to_string(&self)
}
/// This method adds the route specific bundles to the server
/// side rendered HTML.
///
/// The same matching algorithm is implemented on the client side in
/// this file (packages/tuono/src/router/components/Matches.ts).
///
/// Optimizations should occour on both.
fn add_bundle_sources(&mut self) {
// Manifest should always be loaded. The load happen before starting
// the server.
let manifest = MANIFEST.get().expect("Failed to load manifest");
// The main bundle should always exist.
// The extension should be tsx even with JS only projects.
let main_bundle = manifest
.get("client-main")
.expect("Failed to get client-main bundle");
let mut js_bundles_sources = vec![&main_bundle.file];
let mut css_bundles_sources = main_bundle.css.iter().collect::<Vec<&String>>();
let pathname = &self.router.pathname();
let bundle_data = manifest.get(*pathname);
if let Some(data) = bundle_data {
js_bundles_sources.push(&data.file);
data.css
.iter()
.for_each(|source| css_bundles_sources.push(source))
} else {
let dynamic_routes = manifest
.keys()
.filter(|path| has_dynamic_path(path))
.collect::<Vec<&String>>();
if !dynamic_routes.is_empty() {
let path_segments = pathname
.split('/')
.filter(|path| !path.is_empty())
.collect::<Vec<&str>>();
for dyn_route in dynamic_routes.iter() {
let dyn_route_segments = dyn_route
.split('/')
.filter(|path| !path.is_empty())
.collect::<Vec<&str>>();
let mut route_segments_collector: Vec<&str> = vec![];
for i in 0..dyn_route_segments.len() {
if path_segments.len() == i {
break;
}
if dyn_route_segments[i] == path_segments[i]
|| has_dynamic_path(dyn_route_segments[i])
{
route_segments_collector.push(dyn_route_segments[i])
} else {
break;
}
}
if route_segments_collector.len() == path_segments.len() {
let manifest_key = route_segments_collector.join("/");
let route_data = manifest.get(&format!("/{manifest_key}"));
if let Some(data) = route_data {
js_bundles_sources.push(&data.file);
data.css
.iter()
.for_each(|source| css_bundles_sources.push(source))
}
break;
}
}
}
}
self.js_bundles = Some(js_bundles_sources);
self.css_bundles = Some(css_bundles_sources);
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::Uri;
use std::collections::HashMap;
use crate::manifest::BundleInfo;
fn prepare_payload(uri: Option<&str>, mode: Mode) -> Payload<'static> {
let mut manifest_mock = HashMap::new();
manifest_mock.insert(
"client-main".to_string(),
BundleInfo {
file: "assets/bundled-file.js".to_string(),
css: vec!["assets/bundled-file.css".to_string()],
},
);
manifest_mock.insert(
"/".to_string(),
BundleInfo {
file: "assets/index.js".to_string(),
css: vec!["assets/index.css".to_string()],
},
);
manifest_mock.insert(
"/posts/[post]".to_string(),
BundleInfo {
file: "assets/posts/[post].js".to_string(),
css: vec!["assets/posts/[post].css".to_string()],
},
);
manifest_mock.insert(
"/posts/[post]/[comment]".to_string(),
BundleInfo {
file: "assets/posts/[post]/[comment].js".to_string(),
css: vec!["assets/posts/[post]/[comment].css".to_string()],
},
);
manifest_mock.insert(
"/posts/custom-post".to_string(),
BundleInfo {
file: "assets/custom-post.js".to_string(),
css: vec!["assets/custom-post.css".to_string()],
},
);
manifest_mock.insert(
"/about".to_string(),
BundleInfo {
file: "assets/about.js".to_string(),
css: vec!["assets/about.css".to_string()],
},
);
MANIFEST.get_or_init(|| manifest_mock);
let location = Location::from(
&uri.unwrap_or("http://localhost:3000/")
.parse::<Uri>()
.unwrap(),
);
Payload {
router: location,
props: &None::<Option<()>>,
mode,
js_bundles: None,
css_bundles: None,
}
}
#[test]
fn should_load_the_bundles_on_mode_prod() {
let mut payload = prepare_payload(None, Mode::Prod);
let _ = payload.client_payload();
assert_eq!(
payload.js_bundles,
Some(vec![
&"assets/bundled-file.js".to_string(),
&"assets/index.js".to_string()
])
);
assert_eq!(
payload.css_bundles,
Some(vec![
&"assets/bundled-file.css".to_string(),
&"assets/index.css".to_string()
])
);
}
#[test]
fn should_not_load_the_bundles_on_mode_dev() {
let mut payload = prepare_payload(None, Mode::Dev);
let _ = payload.client_payload();
assert!(payload.js_bundles.is_none());
assert!(payload.css_bundles.is_none());
}
#[test]
fn should_load_the_correct_single_dyn_path_bundles() {
let mut payload = prepare_payload(Some("http://localhost:3000/posts/a-post"), Mode::Prod);
let _ = payload.client_payload();
assert_eq!(
payload.js_bundles,
Some(vec![
&"assets/bundled-file.js".to_string(),
&"assets/posts/[post].js".to_string()
])
);
assert_eq!(
payload.css_bundles,
Some(vec![
&"assets/bundled-file.css".to_string(),
&"assets/posts/[post].css".to_string()
])
);
}
#[test]
fn should_load_the_correct_nested_dyn_path_bundles() {
let mut payload = prepare_payload(
Some("http://localhost:3000/posts/a-post/a-comment"),
Mode::Prod,
);
let _ = payload.client_payload();
assert_eq!(
payload.js_bundles,
Some(vec![
&"assets/bundled-file.js".to_string(),
&"assets/posts/[post]/[comment].js".to_string()
])
);
assert_eq!(
payload.css_bundles,
Some(vec![
&"assets/bundled-file.css".to_string(),
&"assets/posts/[post]/[comment].css".to_string()
])
);
}
#[test]
fn should_load_the_defined_path_bundles() {
let mut payload = prepare_payload(Some("http://localhost:3000/about"), Mode::Prod);
let _ = payload.client_payload();
assert_eq!(
payload.js_bundles,
Some(vec![
&"assets/bundled-file.js".to_string(),
&"assets/about.js".to_string()
])
);
assert_eq!(
payload.css_bundles,
Some(vec![
&"assets/bundled-file.css".to_string(),
&"assets/about.css".to_string()
])
);
}
}
+31 -20
View File
@@ -3,6 +3,35 @@ use std::collections::HashMap;
use axum::http::{HeaderMap, Uri};
/// Location must match client side interface
#[derive(Serialize, Debug)]
pub struct Location {
href: String,
pathname: String,
search: HashMap<String, String>,
search_str: String,
hash: String,
}
impl Location {
pub fn pathname(&self) -> &String {
&self.pathname
}
}
impl<'a> From<&'a Uri> for Location {
fn from(uri: &Uri) -> Self {
Location {
href: uri.to_string(),
pathname: uri.path().to_string(),
// TODO: handler search map
search: HashMap::new(),
search_str: uri.query().unwrap_or("").to_string(),
hash: "".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct Request<'a> {
uri: &'a Uri,
@@ -10,17 +39,6 @@ pub struct Request<'a> {
pub params: HashMap<String, String>,
}
/// Location must match client side interface
#[derive(Serialize, Debug)]
pub struct Location<'a> {
href: String,
pathname: &'a str,
search: HashMap<String, String>,
search_str: &'a str,
/// Server does not need it. Will be hanlder client side
hash: &'a str,
}
impl<'a> Request<'a> {
pub fn new(
uri: &'a Uri,
@@ -34,14 +52,7 @@ impl<'a> Request<'a> {
}
}
pub fn location(&self) -> Location<'a> {
Location {
href: self.uri.to_string(),
pathname: &self.uri.path(),
// TODO: handler search map
search: HashMap::new(),
search_str: &self.uri.query().unwrap_or(""),
hash: "",
}
pub fn location(&self) -> Location {
Location::from(self.uri)
}
}
+13 -3
View File
@@ -1,16 +1,26 @@
use crate::{Mode, GLOBAL_MODE};
use lazy_static::lazy_static;
use ssr_rs::Ssr;
use std::cell::RefCell;
use std::fs::read_to_string;
use std::path::PathBuf;
lazy_static! {
static ref BUNDLE_PATH: &'static str = {
if GLOBAL_MODE.get().unwrap() == &Mode::Dev {
return "./.tuono/server/dev-server.js";
}
"./out/server/prod-server.js"
};
}
pub struct Js;
impl Js {
thread_local! {
pub static SSR: RefCell<Ssr<'static, 'static>> = RefCell::new(
// TODO: handle here dev/prod source
Ssr::from(
read_to_string("./.tuono/server/dev-server.js").unwrap(),
""
read_to_string(PathBuf::from(*BUNDLE_PATH)).expect("Server bundle not found"), ""
).unwrap()
)
}
+36 -2
View File
@@ -4,7 +4,13 @@ This tutorial is meant for giving you a sneak peek of the framework and is inten
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 have already installed the tuono CLI you can download the tutorial source with `tuono new tuono-tutorial --template tutorial`
> I'd love to hear your thoughts about the framework and the tutorial - feel free to reach me
at [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it) or in Twitter (X) DMs
[@valerioageno](https://twitter.com/valerioageno
This tutorial is **not meant** for people that don't know React - in that case I suggest you to first read the [React doc](https://react.dev/);
Typescript and Rust knowledge is not a requirement though!
## Table of Content
@@ -17,6 +23,7 @@ The first part is about the project setup and the base knowledge needed to work
* [Create a stand-alone component](#create-a-stand-alone-component)
* [Create the /pokemons/[pokemon] route](#create-the-pokemonspokemon-route)
* [Error handling](#error-handling)
* [Building for production](#building-for-production)
* [Conclusion](#conclusion)
## CLI Installation
@@ -99,6 +106,9 @@ Now that we have some knowledge about the project structure lets start the re
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.
> If you have already installed the tuono CLI and you prefer read the code instead of writing it yourself
you can download the tutorial source with `tuono new tuono-tutorial --template tutorial`
## 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.
@@ -532,9 +542,33 @@ async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response
If you now try to load a not existing pokemon (`http://localhost:3000/pokemons/tuono-pokemon`) you will
correctly receive a 404 status code in the console.
## Building for production
The source now is ready to be released. Both server and client have been managed in a unoptimized way
to easy the development experience. To build the project to the production state just run:
```shell
$ tuono build
```
This command just created the final assets within the `out` directory. To run then the prodiction server
run:
```shell
$ cargo run --release
```
Check again [`http://localhost:3000/`](http://localhost:3000/) - This environment now has all the
optimizations ready to unleash the power of a rust server that seamessly renders a React application!🚀
> Note: The `out` directory is not standalone. You can't rely just on it to run the production server.
## 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
The project is still under heavy development and many features are not ready yet but
I hope you got the taste of what is like working with rust and react in the same stack.
As I mentioned in the introduction I'd love to hear what you thought about the framework and the tutorial - feel free to reach me
at [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it) or in Twitter (X) DMs [@valerioageno](https://twitter.com/valerioageno).
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "workspace",
"packageManager": "pnpm@9.1.1",
"scripts": {
"dev": "turbo dev",
"dev": "turbo dev --filter tuono",
"build": "turbo build",
"lint": "turbo lint",
"format": "turbo format",
+1 -1
View File
@@ -77,7 +77,7 @@ export function buildProd() {
...BASE_CONFIG,
build: {
ssr: true,
minify: false,
minify: true,
outDir: '../out/server',
emptyOutDir: true,
rollupOptions: {
@@ -12,6 +12,14 @@ interface MatchesProps {
const DYNAMIC_PATH_REGEX = /\[(.*?)\]/
/*
* This function is also implemented on server side to match the bundle
* file to load at the first rendering.
*
* File: crates/tuono_lib/src/payload.rs
*
* Optimizations should occour on both
*/
export function getRouteByPathname(pathname: string): Route | undefined {
const { routesById } = useRouter()
+31 -11
View File
@@ -5,33 +5,52 @@ import { RouterProvider } from '../router'
import { createRouter } from '../router'
type RouteTree = any
type Mode = 'Dev' | 'Prod'
const VITE_DEV_AND_HMR = `<script type="module">
import RefreshRuntime from 'http://localhost:3001/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" src="http://localhost:3001/@vite/client"></script>
<script type="module" src="http://localhost:3001/client-main.tsx"></script>`
function generateCssLinks(cssBundles: string[], mode: Mode): string {
if (mode === 'Dev') return ''
return cssBundles.reduce((acc, value) => {
return acc + `<link rel="stylesheet" type="text/css" href="/${value}" />\n`
}, '')
}
function generateJsScripts(jsBundles: string[], mode: Mode): string {
if (mode === 'Dev') return ''
return jsBundles.reduce((acc, value) => {
return acc + `<script type="module" src="/${value}"></script>\n`
}, '')
}
export function serverSideRendering(routeTree: RouteTree) {
return function render(payload: string | undefined): string {
const props = payload ? JSON.parse(payload) : {}
const mode = props.mode as Mode
const jsBundles = props.jsBundles as string[]
const cssBundles = props.cssBundles as string[]
const router = createRouter({ routeTree }) // Render the app
const app = renderToString(
<RouterProvider router={router} serverProps={props} />,
)
const developmentScript = `<script type="module">
import RefreshRuntime from 'http://localhost:3001/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" src="http://localhost:3001/@vite/client"></script>
<script type="module" src="http://localhost:3001/client-main.tsx"></script>`
return `<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Playground</title>
${generateCssLinks(cssBundles, mode)}
</head>
<body>
<div id="__tuono">${app}</div>
@@ -42,7 +61,8 @@ export function serverSideRendering(routeTree: RouteTree) {
}}
/>,
)}
${developmentScript}
${generateJsScripts(jsBundles, mode)}
${mode === 'Dev' ? VITE_DEV_AND_HMR : ''}
</body>
</html>
`