mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-27 13:52:47 -07:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee092194d1 | |||
| 47a02cc572 | |||
| 1849c49682 | |||
| 9f071d9e13 | |||
| b96fdc5c4e | |||
| 89ffcf0923 | |||
| b91210084f | |||
| cfdd59f5fb | |||
| 5a30f36eb0 | |||
| 322908145e | |||
| ec2fb17f7f | |||
| 9a0bb45a03 | |||
| 04216d93bf | |||
| 4ed6c9902f | |||
| 208c18c94a | |||
| d4c9b25b90 | |||
| 74968b27cc | |||
| db98e0e2fd | |||
| fc1d9013f8 | |||
| da75dc4a07 | |||
| 1ef1a852fc | |||
| aa5d4b689d | |||
| 65f43b3caf | |||
| 6ef526be73 | |||
| c55fe7dde4 | |||
| 6d994a1960 | |||
| b557820b6e | |||
| 4e1d0b6252 | |||
| 90de047a07 | |||
| ef98cfed90 | |||
| b21d2eddc0 | |||
| 4a8a24c708 | |||
| 73c94e5fbb | |||
| df80756c44 | |||
| 3830f18e2d | |||
| 289986b4af |
@@ -97,7 +97,7 @@
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-shadow": "error",
|
||||
"no-undef": "error",
|
||||
"no-undef": "off",
|
||||
"sort-imports": "off",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
name: Publish package to npm
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install turbo
|
||||
run: pnpm install turbo --global
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --no-git-checks --filter ./packages/tuono
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -0,0 +1,100 @@
|
||||
name: Publish on npm and crates
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish-on-npm-dry-run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install turbo
|
||||
run: pnpm install turbo --global
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --no-git-checks --dry-run --filter ./packages/tuono
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
publish-on-crates-dry-run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
path: './crates/tuono'
|
||||
dry-run: true
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
check-repo: true
|
||||
|
||||
publish-on-npm:
|
||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install turbo
|
||||
run: pnpm install turbo --global
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --no-git-checks --filter ./packages/tuono
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
publish-on-crates:
|
||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
path: './crates/tuono'
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
check-repo: true
|
||||
+16
@@ -8,3 +8,19 @@ pnpm-lock.yaml
|
||||
|
||||
examples/playground/
|
||||
|
||||
## Rust related ignores
|
||||
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
**/debug/
|
||||
**/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
**/Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/tuono",
|
||||
"crates/tuono_lib",
|
||||
"crates/tuono_lib_macros",
|
||||
]
|
||||
exclude = [
|
||||
"examples/playground"
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2024 Valerio Ageno
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,13 +1,39 @@
|
||||
# Tuono.js
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png">
|
||||
</p>
|
||||
<h1 align="center">Tuono<br>The react/rust fullstack framework</h1>
|
||||
<p align="center">
|
||||
⚠️ This project is under heavy development. API might drastically change ⚠️
|
||||
</p>
|
||||
|
||||
The fullstack react/rust framework.
|
||||
|
||||
> This project is under heavy development. API might drastically change!
|
||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||
Why Tuono? Just a badass name.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
cargo install tuono
|
||||
```
|
||||
|
||||
## Create a new project
|
||||
|
||||
```
|
||||
tuono new
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
tuono dev
|
||||
```
|
||||
|
||||
## Features
|
||||
- Rust based server side rendering
|
||||
- FS routing
|
||||
- CSS modules
|
||||
- Hot Module Reload
|
||||
- Multi thread backend
|
||||
|
||||
## Folder structure
|
||||
|
||||
@@ -18,3 +44,13 @@ The fullstack react/rust framework.
|
||||
| - routes/
|
||||
| | - api/
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Data is passed by the loading function
|
||||
const IndexPage = ({data, isLoading, isError}) => <h1>Index Page</h1>
|
||||
|
||||
export const Route = createFileRoute({
|
||||
loader: (params) => fetchApi(params),
|
||||
component: IndexPage
|
||||
})
|
||||
```
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.0.5"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
homepage = "https://github.com/Valerioageno/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
include = [
|
||||
"src/*.rs",
|
||||
"Cargo.toml"
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "tuono"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
watchexec = "4.0.0"
|
||||
miette = "7.2.0"
|
||||
watchexec-signals = "3.0.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
watchexec-supervisor = "2.0.0"
|
||||
glob = "0.3.1"
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::process::Command;
|
||||
mod axum_source_builder;
|
||||
mod watch;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Actions {
|
||||
/// Start the development environment
|
||||
Dev,
|
||||
/// Build the production assets
|
||||
Build,
|
||||
/// Create a new project folder
|
||||
New,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about = "The react/rust fullstack framework")]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
action: Option<Actions>,
|
||||
}
|
||||
|
||||
pub fn cli() {
|
||||
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();
|
||||
}
|
||||
Actions::New => {
|
||||
println!("Scaffold new project")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
use tuono::cli;
|
||||
|
||||
fn main() {
|
||||
cli();
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use std::sync::Arc;
|
||||
use watchexec_supervisor::command::{Command, Program};
|
||||
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use watchexec::Watchexec;
|
||||
use watchexec_signals::Signal;
|
||||
use watchexec_supervisor::job::start_job;
|
||||
|
||||
use crate::axum_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 {
|
||||
program: Program::Exec {
|
||||
prog: "node_modules/.bin/tuono-dev-watch".into(),
|
||||
args: vec![],
|
||||
}
|
||||
.into(),
|
||||
options: Default::default(),
|
||||
}));
|
||||
|
||||
watch_client.start().await;
|
||||
|
||||
let (run_server, _) = start_job(Arc::new(Command {
|
||||
program: Program::Exec {
|
||||
prog: "cargo".into(),
|
||||
args: vec!["run".to_string()],
|
||||
}
|
||||
.into(),
|
||||
options: Default::default(),
|
||||
}));
|
||||
|
||||
let (build_ssr_bundle, _) = start_job(Arc::new(Command {
|
||||
program: Program::Exec {
|
||||
prog: "node_modules/.bin/tuono-dev-ssr".into(),
|
||||
args: vec![],
|
||||
}
|
||||
.into(),
|
||||
options: Default::default(),
|
||||
}));
|
||||
|
||||
build_ssr_bundle.start().await;
|
||||
run_server.start().await;
|
||||
|
||||
let wx = Watchexec::new(move |mut action| {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// if Ctrl-C is received, quit
|
||||
if action.signals().any(|sig| sig == Signal::Interrupt) {
|
||||
action.quit();
|
||||
}
|
||||
|
||||
action
|
||||
})?;
|
||||
|
||||
// watch the current directory
|
||||
wx.config.pathset(["./src"]);
|
||||
|
||||
let _ = wx.main().await.into_diagnostic()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.0.5"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
homepage = "https://github.com/Valerioageno/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
include = [
|
||||
"src/*.rs",
|
||||
"Cargo.toml"
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "tuono_lib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
ssr_rs = "0.5.1"
|
||||
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.5"}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tuonolib
|
||||
|
||||
This project exposes the interfaces to easily handle the backend build.
|
||||
@@ -0,0 +1,11 @@
|
||||
mod payload;
|
||||
mod request;
|
||||
mod response;
|
||||
|
||||
pub mod ssr;
|
||||
|
||||
pub use payload::Payload;
|
||||
pub use request::Request;
|
||||
pub use response::Response;
|
||||
pub use ssr_rs::Ssr;
|
||||
pub use tuono_lib_macros::handler;
|
||||
@@ -0,0 +1,25 @@
|
||||
use erased_serde::Serialize;
|
||||
use serde::Serialize as SerdeSerialize;
|
||||
|
||||
use crate::request::Location;
|
||||
use crate::Request;
|
||||
|
||||
#[derive(SerdeSerialize)]
|
||||
/// This is the data shared to the client
|
||||
pub struct Payload<'a> {
|
||||
router: Location<'a>,
|
||||
props: Box<dyn Serialize>,
|
||||
}
|
||||
|
||||
impl<'a> Payload<'a> {
|
||||
pub fn new(req: &Request<'a>, props: Box<dyn Serialize>) -> Payload<'a> {
|
||||
Payload {
|
||||
router: req.location(),
|
||||
props,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_payload(&self) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(&self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use axum::http::{HeaderMap, Uri};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Request<'a> {
|
||||
uri: &'a Uri,
|
||||
headers: &'a HeaderMap,
|
||||
}
|
||||
|
||||
/// 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, headers: &'a HeaderMap) -> Request<'a> {
|
||||
Request { uri, headers }
|
||||
}
|
||||
|
||||
pub fn location(&self) -> Location<'a> {
|
||||
Location {
|
||||
href: self.uri.to_string(),
|
||||
pathname: &self.uri.path(),
|
||||
// TODO: hanler search map
|
||||
search: HashMap::new(),
|
||||
search_str: &self.uri.query().unwrap_or(""),
|
||||
hash: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
use erased_serde::Serialize;
|
||||
|
||||
pub enum Response {
|
||||
Redirect(String),
|
||||
Props(Box<dyn Serialize>),
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use ssr_rs::Ssr;
|
||||
use std::cell::RefCell;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
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(),
|
||||
""
|
||||
).unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "tuono_lib_macros"
|
||||
version = "0.0.5"
|
||||
edition = "2021"
|
||||
description = "The react/rust fullstack framework"
|
||||
homepage = "https://github.com/Valerioageno/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
include = [
|
||||
"src/*.rs",
|
||||
"Cargo.toml"
|
||||
]
|
||||
|
||||
[lib]
|
||||
crate_type = ["proc-macro"]
|
||||
proc-marco = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "1.0.59", features = ["full"] }
|
||||
quote = "1.0"
|
||||
@@ -0,0 +1,58 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, ItemFn};
|
||||
|
||||
pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let item = parse_macro_input!(item as ItemFn);
|
||||
|
||||
let fn_name = item.clone().sig.ident;
|
||||
|
||||
quote! {
|
||||
use axum::response::{Html, IntoResponse};
|
||||
|
||||
#item
|
||||
|
||||
pub async fn route(request: axum::extract::Request) -> Html<String> {
|
||||
let pathname = &request.uri();
|
||||
let headers = &request.headers();
|
||||
|
||||
let req = tuono_lib::Request::new(pathname, headers);
|
||||
|
||||
let local_response = #fn_name(&req);
|
||||
|
||||
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
|
||||
_ => Ok("500 Internal server error".to_string())
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(html) => Html(html),
|
||||
_ => Html("500 internal server error".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn api(request: axum::extract::Request) -> axum::response::Response {
|
||||
let pathname = &request.uri();
|
||||
let headers = &request.headers();
|
||||
|
||||
let req = tuono_lib::Request::new(pathname, headers);
|
||||
|
||||
let local_response = #fn_name(&req);
|
||||
|
||||
let res = match local_response{
|
||||
tuono_lib::Response::Props(val) => return axum::Json(val).into_response(),
|
||||
_ => return axum::Json("").into_response()
|
||||
};
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
extern crate proc_macro;
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
mod handler;
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn handler(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
handler::handler_core(args, item)
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -2,9 +2,10 @@
|
||||
"name": "tuono",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"main": "src/index.js",
|
||||
"packageManager": "pnpm@8.12.1",
|
||||
"scripts": {
|
||||
"dev": "turbo dev",
|
||||
"build": "turbo build",
|
||||
"lint": "turbo lint",
|
||||
"format": "turbo format",
|
||||
@@ -17,6 +18,7 @@
|
||||
"devDependencies": {
|
||||
"@tanstack/config": "^0.7.0",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/react": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
||||
"@typescript-eslint/parser": "^7.7.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
@@ -28,7 +30,7 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"prettier": "^3.2.4",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.10",
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^1.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
|
||||
- `tuono-vite`: Handles the bundling (with chunk splitting)
|
||||
- `tuono-routes-generator`: Handles the creation of the `routeGen.tree.ts` file
|
||||
- `tuono-router`: Expose all the Functions and Components related to the routing
|
||||
- `tuono`: Collect and expose all the public APIs. Is the only published package
|
||||
- `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
|
||||
|
||||
@@ -1,12 +1,44 @@
|
||||
{
|
||||
"name": "tuono-router",
|
||||
"version": "1.0.0",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"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"
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"jsdom": "^24.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"zustand": "4.4.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useRouterStore } from '../hooks/useRouterStore'
|
||||
import type { AnchorHTMLAttributes, MouseEvent } from 'react'
|
||||
|
||||
export default function Link(
|
||||
props: AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
): JSX.Element {
|
||||
const handleTransition = (e: MouseEvent<HTMLAnchorElement>): void => {
|
||||
e.preventDefault()
|
||||
props.onClick?.(e)
|
||||
useRouterStore.setState({ location: { pathname: props.href || '' } })
|
||||
window.history.pushState('', '', props.href)
|
||||
}
|
||||
return (
|
||||
<a {...props} onClick={handleTransition}>
|
||||
{props.children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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} />
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useRouter } from '../hooks/useRouter'
|
||||
import { RouteMatch } from './RouteMatch'
|
||||
import Link from './Link'
|
||||
|
||||
export default function NotFound(): JSX.Element {
|
||||
const router = useRouter()
|
||||
|
||||
const custom404Route = router.routesById['/404']
|
||||
|
||||
// Check if exists a custom 404 error page
|
||||
if (custom404Route) {
|
||||
return <RouteMatch route={custom404Route} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>404 Not found</h1>
|
||||
<Link href="/">Return home</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Route } from '../route'
|
||||
import { useServerSideProps } from '../hooks/useServerSideProps'
|
||||
|
||||
interface MatchProps {
|
||||
route: Route
|
||||
// User defined server side props
|
||||
serverSideProps: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the route match with the root element if exists
|
||||
*
|
||||
* It handles the fetch of the client side resources
|
||||
*/
|
||||
export const RouteMatch = ({
|
||||
route,
|
||||
serverSideProps,
|
||||
}: MatchProps): JSX.Element => {
|
||||
const { data, isLoading } = useServerSideProps(route, serverSideProps)
|
||||
|
||||
if (!route.isRoot) {
|
||||
return route.options.getParentRoute().component({
|
||||
children: route.options.component({ data, isLoading }),
|
||||
data,
|
||||
isLoading,
|
||||
})
|
||||
}
|
||||
|
||||
return route.options.component({ data, isLoading })
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react'
|
||||
import type { Router } from '../router'
|
||||
|
||||
const routerContext = React.createContext<Router>(null!)
|
||||
|
||||
const TUONO_CONTEXT = '__TUONO_CONTEXT__'
|
||||
|
||||
export function getRouterContext(): any {
|
||||
if (typeof document === 'undefined') {
|
||||
return routerContext
|
||||
}
|
||||
|
||||
if (window[TUONO_CONTEXT as any]) {
|
||||
return window[TUONO_CONTEXT as any]
|
||||
}
|
||||
|
||||
window[TUONO_CONTEXT as any] = routerContext as any
|
||||
|
||||
return routerContext
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { getRouterContext } from './RouterContext'
|
||||
import { Matches } from './Matches'
|
||||
import { useRouterStore } from '../hooks/useRouterStore'
|
||||
import React, { useLayoutEffect, type ReactNode } from 'react'
|
||||
|
||||
type Router = any
|
||||
|
||||
interface RouterContextProviderProps {
|
||||
router: Router
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function RouterContextProvider({
|
||||
router,
|
||||
children,
|
||||
...rest
|
||||
}: RouterContextProviderProps): JSX.Element {
|
||||
// Allow the router to update options on the router instance
|
||||
router.update({
|
||||
...router.options,
|
||||
...rest,
|
||||
context: {
|
||||
...router.options.context,
|
||||
...rest.context,
|
||||
},
|
||||
})
|
||||
|
||||
const routerContext = getRouterContext()
|
||||
|
||||
const pendingElement = router.options.defaultPendingComponent ? (
|
||||
<router.options.defaultPendingComponent />
|
||||
) : null
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={pendingElement}>
|
||||
<routerContext.Provider value={router}>{children}</routerContext.Provider>
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
interface RouterProviderProps {
|
||||
router: Router
|
||||
serverProps?: ServerProps
|
||||
}
|
||||
|
||||
interface ServerProps {
|
||||
router: Location
|
||||
props: any
|
||||
}
|
||||
|
||||
const initRouterStore = (props?: ServerProps): void => {
|
||||
const updateLocation = useRouterStore((st) => st.updateLocation)
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
updateLocation({
|
||||
pathname: props?.router.pathname || '',
|
||||
hash: '',
|
||||
href: '',
|
||||
searchStr: '',
|
||||
})
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const { pathname, hash, href, search } = window.location
|
||||
updateLocation({
|
||||
pathname,
|
||||
hash,
|
||||
href,
|
||||
searchStr: search,
|
||||
search: new URLSearchParams(search),
|
||||
})
|
||||
}, [])
|
||||
}
|
||||
|
||||
export function RouterProvider({
|
||||
router,
|
||||
serverProps,
|
||||
}: RouterProviderProps): JSX.Element {
|
||||
initRouterStore(serverProps)
|
||||
|
||||
return (
|
||||
<RouterContextProvider router={router}>
|
||||
<Matches serverSideProps={serverProps?.props} />
|
||||
</RouterContextProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import { getRouterContext } from '../components/RouterContext'
|
||||
import type { Router } from '../router'
|
||||
|
||||
export function useRouter(): Router {
|
||||
return React.useContext(getRouterContext())
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
export interface ParsedLocation {
|
||||
href: string
|
||||
pathname: string
|
||||
search?: URLSearchParams
|
||||
searchStr: string
|
||||
hash: string
|
||||
}
|
||||
|
||||
interface RouterState {
|
||||
isLoading: boolean
|
||||
isTransitioning: boolean
|
||||
status: 'idle'
|
||||
location: ParsedLocation
|
||||
matches: string[]
|
||||
pendingMatches: string[]
|
||||
cachedMatches: string[]
|
||||
statusCode: 200
|
||||
updateLocation: (loc: ParsedLocation) => void
|
||||
}
|
||||
|
||||
export const useRouterStore = create<RouterState>()((set) => ({
|
||||
isLoading: false,
|
||||
isTransitioning: false,
|
||||
status: 'idle',
|
||||
location: {
|
||||
href: '',
|
||||
pathname: '',
|
||||
search: undefined,
|
||||
searchStr: '',
|
||||
hash: '',
|
||||
},
|
||||
matches: [],
|
||||
pendingMatches: [],
|
||||
cachedMatches: [],
|
||||
statusCode: 200,
|
||||
updateLocation: (location: ParsedLocation): void => set({ location }),
|
||||
}))
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import type { Route } from '../route'
|
||||
|
||||
const isServer = typeof document === 'undefined'
|
||||
|
||||
interface UseServerSidePropsReturn {
|
||||
data: any
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
/*
|
||||
* Use the props provided by the SSR and dehydrate the
|
||||
* props for client side usage.
|
||||
*
|
||||
* In case is a client fetch the remote data API
|
||||
*/
|
||||
export function useServerSideProps(
|
||||
route: Route,
|
||||
// User defined props
|
||||
serverSideProps: any,
|
||||
): UseServerSidePropsReturn {
|
||||
const isFirstRendering = useRef<boolean>(true)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(
|
||||
// Force loading if has handler
|
||||
route.options.hasHandler &&
|
||||
// Avoid loading on the server
|
||||
!isServer &&
|
||||
// Avoid loading if first rendering
|
||||
!isFirstRendering.current,
|
||||
)
|
||||
|
||||
const [data, setData] = useState<any>(
|
||||
serverSideProps ?? window.__TUONO_SSR_PROPS__?.props,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// First loading just dehydrate since the
|
||||
// props are already bundled by the SSR
|
||||
if (isFirstRendering.current) {
|
||||
isFirstRendering.current = false
|
||||
return
|
||||
}
|
||||
// After client side routing load again the remote data
|
||||
if (route.options.hasHandler && !data) {
|
||||
;(async (): Promise<void> => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const res = await fetch(`/__tuono/data/${route.path}`)
|
||||
setData(await res.json())
|
||||
} catch (error) {
|
||||
// Handle here error
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
// Clean up the data when changing route
|
||||
return (): void => {
|
||||
setData(undefined)
|
||||
}
|
||||
}, [route.path])
|
||||
|
||||
return { isLoading, data }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
__TUONO_SSR__PROPS__: any
|
||||
}
|
||||
}
|
||||
export { RouterProvider } from './components/RouterProvider'
|
||||
export { default as Link } from './components/Link'
|
||||
export { createRouter } from './router'
|
||||
export { createRoute, createRootRoute, getRouteApi } from './route'
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { RouterType } from './router'
|
||||
import { trimPathLeft, joinPaths } from './utils'
|
||||
|
||||
interface RouteOptions {
|
||||
isRoot?: boolean
|
||||
getParentRoute?: () => Route
|
||||
path?: string
|
||||
component: () => JSX.Element
|
||||
}
|
||||
|
||||
export function createRoute(options: RouteOptions): Route {
|
||||
return new Route(options)
|
||||
}
|
||||
|
||||
export const rootRouteId = '__root__'
|
||||
|
||||
export class Route {
|
||||
parentRoute!: any
|
||||
id: number
|
||||
fullPath!: string
|
||||
path: string
|
||||
options: any
|
||||
|
||||
children?: Route[]
|
||||
router: RouterType
|
||||
isRoot: boolean
|
||||
originalIndex: number
|
||||
component: () => JSX.Element
|
||||
|
||||
constructor(options: RouteOptions) {
|
||||
this.isRoot = options.isRoot ?? typeof options.getParentRoute !== 'function'
|
||||
this.options = options
|
||||
;(this as any).$$typeof = Symbol.for('react.memo')
|
||||
|
||||
this.component = options.component
|
||||
}
|
||||
|
||||
init = (originalIndex: number): void => {
|
||||
this.originalIndex = originalIndex
|
||||
|
||||
const isRoot = !this.options?.path && !this.options?.id
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
this.parentRoute = this.options?.getParentRoute?.()
|
||||
|
||||
if (isRoot) {
|
||||
this.path = rootRouteId
|
||||
}
|
||||
|
||||
let path: undefined | string = isRoot ? rootRouteId : this.options.path
|
||||
|
||||
// If the path is anything other than an index path, trim it up
|
||||
if (path && path !== '/') {
|
||||
path = trimPathLeft(path)
|
||||
}
|
||||
|
||||
const customId = this.options?.id || path
|
||||
|
||||
// Strip the parentId prefix from the first level of children
|
||||
let id = isRoot
|
||||
? rootRouteId
|
||||
: joinPaths([
|
||||
this.parentRoute.id === rootRouteId ? '' : this.parentRoute.id,
|
||||
customId,
|
||||
])
|
||||
|
||||
if (path === rootRouteId) {
|
||||
path = '/'
|
||||
}
|
||||
|
||||
if (id !== rootRouteId) {
|
||||
id = joinPaths(['/', id])
|
||||
}
|
||||
|
||||
const fullPath =
|
||||
id === rootRouteId ? '/' : joinPaths([this.parentRoute.fullPath, path])
|
||||
|
||||
this.path = path as TPath
|
||||
this.id = id as TId
|
||||
// this.customId = customId as TCustomId
|
||||
this.fullPath = fullPath as TFullPath
|
||||
this.to = fullPath as TrimPathRight<TFullPath>
|
||||
}
|
||||
|
||||
addChildren(routes: Route[]): Route {
|
||||
this.children = routes
|
||||
return this
|
||||
}
|
||||
|
||||
update = (options: RouteOptions): this => {
|
||||
Object.assign(this.options, options)
|
||||
this.isRoot = options.isRoot || !options.getParentRoute
|
||||
return this
|
||||
}
|
||||
|
||||
useRouteContext = () => {}
|
||||
|
||||
useParams = () => {}
|
||||
}
|
||||
|
||||
export function createRootRoute(options?: RouteOptions): Route {
|
||||
return new Route({ ...options, isRoot: true })
|
||||
}
|
||||
|
||||
export function getRouteApi(id: string): RouteApi {
|
||||
return new RouteApi(id)
|
||||
}
|
||||
|
||||
class RouteApi {
|
||||
id: string
|
||||
|
||||
constructor(id: string) {
|
||||
this.id = id
|
||||
}
|
||||
|
||||
useParams = () => {}
|
||||
|
||||
useRouteContext = () => {}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { trimPath, trimPathRight } from './utils'
|
||||
import type { Route } from './route'
|
||||
|
||||
type RouteTree = any
|
||||
|
||||
interface CreateRouter {
|
||||
routeTree: RouteTree
|
||||
basePath?: string
|
||||
options: RouteOptions
|
||||
}
|
||||
|
||||
interface RouteOptions {
|
||||
component?: () => JSX.Element
|
||||
hasHandler?: boolean
|
||||
routeTree?: RouteTree
|
||||
}
|
||||
export type RouterType = any
|
||||
|
||||
export function createRouter(options: CreateRouterArgs): Router {
|
||||
return new Router(options)
|
||||
}
|
||||
|
||||
export class Router {
|
||||
options?: RouteOptions
|
||||
basePath = '/'
|
||||
routeTree: any
|
||||
history: any
|
||||
isServer = typeof document === 'undefined'
|
||||
routesById: Record<string, Route> = {}
|
||||
// Not used
|
||||
routesByPath: Record<string, Route> = {}
|
||||
flatRoutes: any
|
||||
|
||||
constructor(options: CreateRouter) {
|
||||
this.update({
|
||||
...options,
|
||||
})
|
||||
|
||||
if (!this.isServer) {
|
||||
;(window as any).__TSR__ROUTER__ = this
|
||||
}
|
||||
}
|
||||
|
||||
update = (newOptions: CreateRouter): void => {
|
||||
this.options = {
|
||||
...this.options,
|
||||
...newOptions,
|
||||
}
|
||||
|
||||
this.#updateBasePath(newOptions.basePath)
|
||||
|
||||
// NOTE: next iteration
|
||||
this.#historyUpdate()
|
||||
|
||||
// NOTE: next iteration
|
||||
this.#storeUpdate()
|
||||
|
||||
if (this.options.routeTree !== this.routeTree) {
|
||||
this.routeTree = this.options.routeTree
|
||||
this.#buildRouteTree()
|
||||
}
|
||||
}
|
||||
|
||||
#historyUpdate = (): void => {
|
||||
// TODO: update history
|
||||
}
|
||||
|
||||
#storeUpdate = (): void => {
|
||||
// TODO: update store
|
||||
}
|
||||
|
||||
#buildRouteTree = (): void => {
|
||||
const recurseRoutes = (childRoutes: Route[]): void => {
|
||||
childRoutes.forEach((route: Route, i: number) => {
|
||||
route.init(i)
|
||||
|
||||
this.routesById[route.id] = route
|
||||
|
||||
if (!route.isRoot && route.options.path) {
|
||||
const trimmedFullPath = trimPathRight(route.fullPath)
|
||||
if (
|
||||
!this.routesByPath[trimmedFullPath] ||
|
||||
route.fullPath.endsWith('/')
|
||||
) {
|
||||
this.routesByPath[trimmedFullPath] = route
|
||||
}
|
||||
}
|
||||
|
||||
const children = route.children
|
||||
if (children?.length) {
|
||||
recurseRoutes(children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
recurseRoutes([this.routeTree])
|
||||
}
|
||||
|
||||
#updateBasePath = (basePath?: string): void => {
|
||||
if (!this.basePath || (basePath && basePath !== basePath)) {
|
||||
if (basePath === undefined || basePath === '' || basePath === '/') {
|
||||
this.basePath = '/'
|
||||
} else {
|
||||
this.basePath = `/${trimPath(basePath)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface Segment {
|
||||
type: 'pathname' | 'param' | 'wildcard'
|
||||
value: string
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Segment } from './types'
|
||||
|
||||
export function joinPaths(paths: (string | undefined)[]): string {
|
||||
return cleanPath(paths.filter(Boolean).join('/'))
|
||||
}
|
||||
|
||||
function cleanPath(path: string): string {
|
||||
// remove double slashes
|
||||
return path.replace(/\/{2,}/g, '/')
|
||||
}
|
||||
|
||||
export function parsePathname(pathname?: string): Segment[] {
|
||||
if (!pathname) {
|
||||
return []
|
||||
}
|
||||
|
||||
pathname = cleanPath(pathname)
|
||||
|
||||
const segments: Segment[] = []
|
||||
|
||||
if (pathname.slice(0, 1) === '/') {
|
||||
pathname = pathname.substring(1)
|
||||
segments.push({
|
||||
type: 'pathname',
|
||||
value: '/',
|
||||
})
|
||||
}
|
||||
|
||||
if (!pathname) {
|
||||
return segments
|
||||
}
|
||||
|
||||
// Remove empty segments and '.' segments
|
||||
const split = pathname.split('/').filter(Boolean)
|
||||
|
||||
segments.push(
|
||||
...split.map((part): Segment => {
|
||||
if (part === '$' || part === '*') {
|
||||
return {
|
||||
type: 'wildcard',
|
||||
value: part,
|
||||
}
|
||||
}
|
||||
|
||||
if (part.charAt(0) === '$') {
|
||||
return {
|
||||
type: 'param',
|
||||
value: part,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'pathname',
|
||||
value: part,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
if (pathname.slice(-1) === '/') {
|
||||
pathname = pathname.substring(1)
|
||||
segments.push({
|
||||
type: 'pathname',
|
||||
value: '/',
|
||||
})
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
export function trimPathLeft(path: string): string {
|
||||
return path === '/' ? path : path.replace(/^\/{1,}/, '')
|
||||
}
|
||||
export function trimPathRight(path: string): string {
|
||||
return path === '/' ? path : path.replace(/\/{1,}$/, '')
|
||||
}
|
||||
export function trimPath(path: string): string {
|
||||
return trimPathRight(trimPathLeft(path))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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']
|
||||
}>()
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
/* 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()
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"jsx": "react-jsx",
|
||||
},
|
||||
"include": ["src", "tests", "vite.config.ts"],
|
||||
}
|
||||
|
||||
@@ -18,4 +18,3 @@ export default mergeConfig(
|
||||
srcDir: './src',
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"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 '**/*'",
|
||||
@@ -28,7 +29,9 @@
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"prettier": "^3.2.4"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
|
||||
@@ -34,8 +34,11 @@ const defaultConfig: Config = {
|
||||
let isFirst = false
|
||||
let skipMessage = false
|
||||
|
||||
async function getRouteNodes(config = defaultConfig): Promise<RouteNode[]> {
|
||||
async function getRouteNodes(
|
||||
config = defaultConfig,
|
||||
): Promise<{ routeNodes: RouteNode[]; rustHandlersNodes: string[] }> {
|
||||
const routeNodes: RouteNode[] = []
|
||||
const rustHandlersNodes: string[] = []
|
||||
|
||||
async function recurse(dir: string): Promise<void> {
|
||||
const fullDir = path.resolve(config.folderName, dir)
|
||||
@@ -74,6 +77,19 @@ async function getRouteNodes(config = defaultConfig): Promise<RouteNode[]> {
|
||||
isLoader,
|
||||
variableName,
|
||||
})
|
||||
} else if (fullPath.match(/\.(rs)$/)) {
|
||||
const filePath = replaceBackslash(path.join(dir, dirent.name))
|
||||
const filePathNoExt = removeExt(filePath)
|
||||
let routePath =
|
||||
cleanPath(`/${filePathNoExt.split('.').join('/')}`) || ''
|
||||
|
||||
if (routePath === 'index') {
|
||||
routePath = '/'
|
||||
}
|
||||
|
||||
routePath = routePath.replace(/\/index$/, '/') || '/'
|
||||
|
||||
rustHandlersNodes.push(routePath)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -81,7 +97,7 @@ async function getRouteNodes(config = defaultConfig): Promise<RouteNode[]> {
|
||||
|
||||
await recurse('./')
|
||||
|
||||
return routeNodes
|
||||
return { routeNodes, rustHandlersNodes }
|
||||
}
|
||||
|
||||
export function hasParentRoute(
|
||||
@@ -139,7 +155,8 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
latestTask = taskId
|
||||
|
||||
const start = Date.now()
|
||||
const beforeRouteNodes = await getRouteNodes(config)
|
||||
const { routeNodes: beforeRouteNodes, rustHandlersNodes } =
|
||||
await getRouteNodes(config)
|
||||
|
||||
const preRouteNodes = multiSortBy(beforeRouteNodes, [
|
||||
(d): number => (d.routePath === '/' ? -1 : 1),
|
||||
@@ -239,44 +256,45 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
(d): any => d,
|
||||
])
|
||||
|
||||
const routeImports = [
|
||||
'// This file is auto-generated by Tuono',
|
||||
[
|
||||
`import { Route as rootRoute } from './${replaceBackslash(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, ROOT_PATH_ID),
|
||||
),
|
||||
)}'`,
|
||||
...sortedRouteNodes.map((node) => {
|
||||
return `import { Route as ${
|
||||
node.variableName
|
||||
}Import } from './${replaceBackslash(
|
||||
removeExt(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, node.filePath),
|
||||
),
|
||||
false,
|
||||
const imports = [
|
||||
...sortedRouteNodes.map((node) => {
|
||||
return `import ${node.variableName}Import from './${replaceBackslash(
|
||||
removeExt(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, node.filePath),
|
||||
),
|
||||
)}'`
|
||||
}),
|
||||
].join('\n'),
|
||||
'// Create/Update Routes',
|
||||
false,
|
||||
),
|
||||
)}'`
|
||||
}),
|
||||
].join('\n')
|
||||
|
||||
const createRoutes = [
|
||||
...sortedRouteNodes.map((node) => {
|
||||
return `const ${node.variableName} = createRoute({ component: ${node.variableName}Import })`
|
||||
}),
|
||||
].join('\n')
|
||||
|
||||
const createRouteUpdates = [
|
||||
sortedRouteNodes
|
||||
.map((node) => {
|
||||
const loaderNode = routePiecesByPath[node.routePath]?.loader
|
||||
const lazyComponentNode = routePiecesByPath[node.routePath]?.lazy
|
||||
|
||||
return [
|
||||
`const ${node.variableName}Route = ${node.variableName}Import.update({
|
||||
`const ${node.variableName}Route = ${node.variableName}.update({
|
||||
${[
|
||||
`path: '${node.cleanedPath}'`,
|
||||
`getParentRoute: () => ${node.parent?.variableName ?? 'root'}Route`,
|
||||
rustHandlersNodes.includes(node.path || '')
|
||||
? 'hasHandler: true'
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(',')}
|
||||
} as any)`,
|
||||
// Verify if needed
|
||||
loaderNode
|
||||
? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash(
|
||||
removeExt(
|
||||
@@ -302,25 +320,24 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
].join('')
|
||||
})
|
||||
.join('\n\n'),
|
||||
...[
|
||||
'// Populate the FileRoutesByPath interface',
|
||||
`declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
${routeNodes
|
||||
.map((routeNode) => {
|
||||
return `'${removeTrailingUnderscores(routeNode.routePath)}': {
|
||||
preLoaderRoute: typeof ${routeNode.variableName}Import
|
||||
parentRoute: typeof ${
|
||||
routeNode.parent?.variableName
|
||||
? `${routeNode.parent.variableName}Import`
|
||||
: 'rootRoute'
|
||||
}
|
||||
}`
|
||||
})
|
||||
.join('\n')}
|
||||
}
|
||||
}`,
|
||||
],
|
||||
]
|
||||
|
||||
const routeImports = [
|
||||
'// This file is auto-generated by Tuono',
|
||||
"import { createRoute } from 'tuono'",
|
||||
[
|
||||
`import RootImport from './${replaceBackslash(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, ROOT_PATH_ID),
|
||||
),
|
||||
)}'`,
|
||||
].join('\n'),
|
||||
imports,
|
||||
'const rootRoute = createRoute({ isRoot: true, component: RootImport });',
|
||||
createRoutes,
|
||||
'// Create/Update Routes',
|
||||
createRouteUpdates,
|
||||
'// Create and export the route tree',
|
||||
`export const routeTree = rootRoute.addChildren([${routeConfigChildrenText}])`,
|
||||
]
|
||||
|
||||
@@ -1,56 +1,43 @@
|
||||
// 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'
|
||||
import { Route as PostsIndexImport } from './routes/posts/index'
|
||||
import { Route as PostsPostSlugImport } from './routes/posts/post-slug'
|
||||
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 = AboutImport.update({
|
||||
const AboutRoute = About.update({
|
||||
path: '/about',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
const IndexRoute = Index.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const PostsIndexRoute = PostsIndexImport.update({
|
||||
const PostsIndexRoute = PostsIndex.update({
|
||||
path: '/posts/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const PostsPostSlugRoute = PostsPostSlugImport.update({
|
||||
const PostsPostSlugRoute = PostsPostSlug.update({
|
||||
path: '/posts/post-slug',
|
||||
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
|
||||
}
|
||||
'/posts/post-slug': {
|
||||
preLoaderRoute: typeof PostsPostSlugImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/posts/': {
|
||||
preLoaderRoute: typeof PostsIndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
export const routeTree = rootRoute.addChildren([
|
||||
|
||||
+13
-20
@@ -1,36 +1,29 @@
|
||||
// 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'
|
||||
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 = AboutImport.update({
|
||||
const AboutRoute = About.update({
|
||||
path: '/about',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
const IndexRoute = Index.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])
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// 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])
|
||||
@@ -0,0 +1 @@
|
||||
/** */
|
||||
@@ -0,0 +1 @@
|
||||
/** */
|
||||
@@ -0,0 +1 @@
|
||||
/** */
|
||||
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"name": "tuono-vite",
|
||||
"version": "0.1.0",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"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 '**/*'",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SPLIT_PREFIX } from './constants'
|
||||
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
const ROUTES_DIRECTORY_PATH = 'src/routes'
|
||||
const ROUTES_DIRECTORY_PATH = './src/routes'
|
||||
const DEBUG = true
|
||||
|
||||
let lock = false
|
||||
@@ -40,7 +40,7 @@ export function RouterGenerator(): Plugin {
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-fs-router-generator',
|
||||
name: 'vite-plugin-tuono-fs-router-generator',
|
||||
configResolved: async (): Promise<void> => {
|
||||
await generate()
|
||||
},
|
||||
@@ -57,17 +57,18 @@ export function RouterGenerator(): Plugin {
|
||||
|
||||
export function RouterCodeSplitter(): Plugin {
|
||||
const ROOT: string = process.cwd()
|
||||
console.log('ROOT', ROOT)
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-fs-router-code-splitter',
|
||||
name: 'vite-plugin-tuono-fs-router-code-splitter',
|
||||
enforce: 'pre',
|
||||
resolveId(source): string {
|
||||
resolveId(source): string | null {
|
||||
if (source.startsWith(SPLIT_PREFIX + ':')) {
|
||||
return source.replace(SPLIT_PREFIX + ':', '')
|
||||
}
|
||||
return ''
|
||||
return null
|
||||
},
|
||||
async transform(code, id): Promise<void> {
|
||||
async transform(code, id): Promise<any> {
|
||||
const url = pathToFileURL(id)
|
||||
url.searchParams.delete('v')
|
||||
id = fileURLToPath(url).replace(/\\/g, '/')
|
||||
@@ -77,6 +78,7 @@ export function RouterCodeSplitter(): Plugin {
|
||||
if (DEBUG) console.info('Route: ', id)
|
||||
|
||||
if (id.includes(SPLIT_PREFIX)) {
|
||||
console.log('Split')
|
||||
if (DEBUG) console.info('Splitting route: ', id)
|
||||
|
||||
const compiled = await splitFile({
|
||||
@@ -101,7 +103,11 @@ export function RouterCodeSplitter(): Plugin {
|
||||
}
|
||||
|
||||
return compiled
|
||||
} else {
|
||||
console.log('Non split')
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Tuono
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { buildProd } from '../dist/esm/build/index.js'
|
||||
|
||||
buildProd()
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { developmentSSRBundle } from '../dist/esm/build/index.js'
|
||||
|
||||
developmentSSRBundle()
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { developmentCSRWatch } from '../dist/esm/build/index.js'
|
||||
|
||||
developmentCSRWatch()
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.5",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
||||
"format": "prettier -u --write '**/*'",
|
||||
@@ -14,6 +15,16 @@
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"module": "dist/esm/index.js",
|
||||
"exports": {
|
||||
"./build": {
|
||||
"import": {
|
||||
"types": "./dist/esm/build/index.d.ts",
|
||||
"default": "./dist/esm/build/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/build/index.d.ts",
|
||||
"default": "./dist/cjs/build/index.js"
|
||||
}
|
||||
},
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
@@ -26,11 +37,23 @@
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"bin": {
|
||||
"tuono-dev-ssr": "./bin/dev-ssr.js",
|
||||
"tuono-dev-watch": "./bin/watch.js",
|
||||
"tuono-build-prod": "./bin/build-prod.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md"
|
||||
"README.md",
|
||||
"bin/**"
|
||||
],
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"tuono-router": "workspace:*",
|
||||
"tuono-vite": "workspace:*",
|
||||
"vite": "^5.2.11"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "Valerio Ageno",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { build, createServer } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import { ViteFsRouter } from 'tuono-vite'
|
||||
|
||||
const BASE_CONFIG = {
|
||||
silent: true,
|
||||
root: '.tuono',
|
||||
publicDir: '../public',
|
||||
cacheDir: 'cache',
|
||||
envDir: '../',
|
||||
plugins: [react(), ViteFsRouter()],
|
||||
}
|
||||
|
||||
export function developmentSSRBundle() {
|
||||
;(async () => {
|
||||
console.log('Build SSR')
|
||||
await build({
|
||||
...BASE_CONFIG,
|
||||
build: {
|
||||
ssr: true,
|
||||
minify: false,
|
||||
outDir: 'server',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: './.tuono/server-main.tsx',
|
||||
output: {
|
||||
entryFileNames: 'dev-server.js',
|
||||
format: 'iife',
|
||||
},
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
target: 'webworker',
|
||||
noExternal: true,
|
||||
},
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
export function developmentCSRWatch() {
|
||||
;(async () => {
|
||||
console.log('Watch files')
|
||||
const server = await createServer({
|
||||
...BASE_CONFIG,
|
||||
server: {
|
||||
port: 3001,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
manifest: true,
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: './.tuono/client-main.tsx',
|
||||
},
|
||||
},
|
||||
})
|
||||
await server.listen()
|
||||
})()
|
||||
}
|
||||
|
||||
export function buildProd() {
|
||||
;(async () => {
|
||||
await build({
|
||||
...BASE_CONFIG,
|
||||
manifest: true,
|
||||
build: {
|
||||
outDir: '../out/client',
|
||||
},
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: './.tuono/client-main.tsx',
|
||||
},
|
||||
})
|
||||
|
||||
await build({
|
||||
...BASE_CONFIG,
|
||||
build: {
|
||||
ssr: true,
|
||||
minify: false,
|
||||
outDir: '../out/server',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: './.tuono/server-main.tsx',
|
||||
output: {
|
||||
entryFileNames: 'prod-server.js',
|
||||
format: 'iife',
|
||||
},
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
target: 'webworker',
|
||||
noExternal: true,
|
||||
},
|
||||
})
|
||||
})()
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
export default function sum(a: number, b: number): number {
|
||||
return a + b
|
||||
}
|
||||
import {
|
||||
createRoute,
|
||||
createRootRoute,
|
||||
createRouter,
|
||||
Link,
|
||||
RouterProvider,
|
||||
} from 'tuono-router'
|
||||
|
||||
export { createRoute, createRootRoute, createRouter, Link, RouterProvider }
|
||||
|
||||
@@ -14,7 +14,7 @@ const config = defineConfig({
|
||||
export default mergeConfig(
|
||||
config,
|
||||
tanstackBuildConfig({
|
||||
entry: './src/index.ts',
|
||||
entry: ['./src/index.ts', './src/build/index.ts'],
|
||||
srcDir: './src',
|
||||
}),
|
||||
)
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
pnpm dev & cargo watch -x build -w crates/
|
||||
|
||||
+5
-1
@@ -5,7 +5,11 @@
|
||||
"format": {},
|
||||
"test": {},
|
||||
"build": {
|
||||
"outputs": ["dist/**"]
|
||||
"outputs": ["dist/**"],
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"dev": {
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user