feat(scaffold): add Axum + Leptos SSR base structure

Sets up the full project skeleton: Cargo.toml with ssr/hydrate features,
Axum server entry point, shared Leptos lib, root App component with router,
server/client module split, and Trunk config for WASM build.

Both `cargo check --features ssr` and `cargo check --features hydrate --target wasm32-unknown-unknown` pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 19:16:35 +02:00
parent 11b0f60892
commit efad573c3b
11 changed files with 3128 additions and 3 deletions

2691
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,77 @@
[package] [package]
name = "rust-ipam" name = "rust-ipam"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
# Leptos nécessite deux formats de compilation :
# - rlib : bibliothèque normale, utilisée par le serveur Axum
# - cdylib : bibliothèque dynamique compilée en WebAssembly pour le navigateur
[lib]
crate-type = ["cdylib", "rlib"]
# Les "features" permettent d'activer du code conditionnellement selon le contexte.
# On compile deux fois le même code : une fois en mode "ssr" (serveur), une fois "hydrate" (WASM).
[features]
# Mode serveur : active Axum, Tokio, et le rendu HTML côté serveur
ssr = [
"dep:axum",
"dep:tokio",
"dep:tower-http",
"dep:leptos_axum",
"dep:tracing-subscriber",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
# Mode client : compile l'application en WebAssembly pour le navigateur
# Note : seul `leptos` expose une feature "hydrate" ; leptos_meta et leptos_router
# n'en ont pas besoin — ils s'adaptent automatiquement au mode de compilation.
hydrate = [
"dep:console_error_panic_hook",
"dep:wasm-bindgen",
"leptos/hydrate",
]
[dependencies] [dependencies]
# --- Dépendances partagées (compilées côté serveur ET client) ---
# Framework UI réactif full-stack : le cœur du projet
leptos = { version = "0.7", features = [] }
# Gestion des balises HTML <head> (title, meta tags, liens CSS...)
leptos_meta = { version = "0.7", features = [] }
# Routeur : associe des URLs à des composants, côté serveur et client
leptos_router = { version = "0.7", features = [] }
# Dérive automatiquement des types d'erreurs idiomatiques Rust
thiserror = "1"
# Macros pour les logs : tracing::info!(), tracing::error!()...
tracing = "0.1"
# --- Dépendances serveur uniquement (activées par la feature "ssr") ---
# Serveur HTTP asynchrone rapide et ergonomique
axum = { version = "0.7", optional = true }
# Runtime asynchrone Rust (nécessaire pour `async fn main()` et les Futures)
tokio = { version = "1", features = ["full"], optional = true }
# Pont entre Leptos et Axum : SSR, server functions, streaming...
leptos_axum = { version = "0.7", optional = true }
# Middleware HTTP : sert les fichiers statiques (CSS, WASM compilé, images...)
tower-http = { version = "0.5", features = ["fs"], optional = true }
# Formateur de logs pour le terminal (affiche les messages tracing::info!...)
tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true }
# --- Dépendances client uniquement (activées par la feature "hydrate") ---
# Affiche les panics Rust dans la console du navigateur (indispensable pour déboguer)
console_error_panic_hook = { version = "0.1", optional = true }
# Pont entre Rust/WASM et JavaScript : permet d'appeler du JS depuis Rust
wasm-bindgen = { version = "0.2", optional = true }
# Profil de compilation WASM optimisé pour réduire la taille du fichier .wasm
# Un fichier WASM plus petit = page qui charge plus vite
[profile.wasm-release]
inherits = "release"
opt-level = "z" # Optimise pour la taille (z) plutôt que la vitesse (3)
lto = true # Link-Time Optimization : élimine le code mort entre crates
codegen-units = 1 # Un seul thread de codegen = meilleure optimisation globale
panic = "abort" # Pas de stack unwinding = binaire plus petit

25
Trunk.toml Normal file
View File

@@ -0,0 +1,25 @@
# Trunk.toml — Configuration de trunk
# trunk est l'outil de build pour les applications Rust/WASM.
#
# Commandes principales :
# trunk serve → serveur de dev avec hot-reload (recompile à chaque changement)
# trunk build → compilation production (dans target/site/)
# trunk build --release → compilation production optimisée (avec profile wasm-release)
[build]
# Feature à activer lors de la compilation WASM
# "hydrate" active le code client et désactive le code serveur
features = ["hydrate"]
# Dossier de sortie des fichiers compilés (JS, WASM, CSS, HTML)
dist = "target/site"
[watch]
# Dossiers à ignorer lors de la surveillance des changements de fichiers
# Sans ça, trunk se relancerait en boucle en détectant ses propres fichiers compilés
ignore = ["./target"]
[serve]
# Port du serveur de développement trunk
port = 3000
# Ne pas ouvrir le navigateur automatiquement au démarrage
open = false

25
index.html Normal file
View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--
trunk complète automatiquement ce fichier lors de `trunk build` ou `trunk serve` :
- Il injecte les balises <link> pour charger le CSS compilé
- Il injecte les balises <script> pour charger le bundle WebAssembly
Ne pas ajouter ces balises manuellement ici — trunk le fait pour vous.
-->
</head>
<body>
<!--
Leptos monte l'application ici via mount_to_body() (défini dans lib.rs).
Flux SSR + Hydration :
1. Le navigateur demande la page au serveur Axum
2. Axum rend le composant App() en HTML (SSR) et l'envoie
3. Le navigateur affiche le HTML instantanément (pas d'écran blanc)
4. Le bundle WASM se charge en arrière-plan
5. Leptos "hydrate" le HTML : attache les event listeners pour le rendre interactif
-->
</body>
</html>

62
src/app.rs Normal file
View File

@@ -0,0 +1,62 @@
// app.rs — Composant racine de l'application Leptos
//
// Ce composant est le point d'entrée de toute l'interface.
// Il définit :
// - Les métadonnées globales (title, CSS...)
// - Le routeur : quelle page afficher selon l'URL
// - Les contextes globaux partagés (à ajouter plus tard : auth, thème...)
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{
components::{Route, Router, Routes},
path,
};
use crate::client::home::HomePage;
// `#[component]` est un attribut procédural Leptos.
// Il transforme une fonction Rust normale en composant réutilisable et traçable.
//
// Règle de nommage : toujours PascalCase pour les composants Leptos.
//
// `-> impl IntoView` : la fonction retourne "quelque chose affichable".
// On utilise `impl` (type opaque) car le type exact généré par `view!` est complexe.
#[component]
pub fn App() -> impl IntoView {
// Initialise le contexte de métadonnées Leptos.
// Sans cet appel, les composants <Title>, <Meta>, <Stylesheet> plus bas ne fonctionnent pas.
provide_meta_context();
// La macro `view!` permet d'écrire du HTML dans du Rust.
// Leptos la transforme en code Rust pur à la compilation — pas de runtime template engine.
view! {
// Définit le titre de l'onglet navigateur (injecté dans le <head> HTML)
<Title text="Rust IPAM — Gestionnaire d'adresses IP"/>
// Charge le CSS global. Le nom de fichier suit la convention Leptos :
// `{nom-du-crate}.css` dans le dossier `/pkg/` servi par Axum.
<Stylesheet id="main" href="/pkg/rust-ipam.css"/>
// Le Router gère la navigation côté client sans rechargement de page.
// Côté serveur (SSR), il détermine quel composant rendre selon l'URL demandée.
<Router>
<main>
// `<Routes>` est le conteneur pour toutes les définitions de routes.
// `fallback` est affiché si aucune route ne correspond à l'URL actuelle.
<Routes fallback=|| view! {
<div class="page-erreur">
<h1>"404 — Page introuvable"</h1>
<a href="/">"← Retour à l'accueil"</a>
</div>
}>
// Chaque `<Route>` associe un chemin URL à un composant.
// `path!(/)` correspond exactement à l'URL racine "/".
// Ajouter de nouvelles pages ici, exemple :
// <Route path=path!("/reseaux") view=ReseauxPage/>
<Route path=path!("/") view=HomePage/>
</Routes>
</main>
</Router>
}
}

78
src/client/home.rs Normal file
View File

@@ -0,0 +1,78 @@
// client/home.rs — Page d'accueil
//
// Ce fichier illustre les concepts fondamentaux de Leptos :
// - Composants (`#[component]`)
// - Signals (valeurs réactives)
// - Memos (valeurs dérivées)
// - Gestion d'événements (`on:click`)
use leptos::prelude::*;
// Composant de la page d'accueil.
//
// En Leptos, un composant est une simple fonction Rust avec l'attribut `#[component]`.
// Elle est appelée une seule fois pour construire le graphe réactif —
// ce n'est pas comme React où le composant se "ré-exécute" à chaque mise à jour.
#[component]
pub fn HomePage() -> impl IntoView {
// `RwSignal<T>` (Read-Write Signal) est une valeur réactive mutable.
//
// Quand on appelle `.set()` ou `.update()`, Leptos identifie automatiquement
// tous les éléments du DOM qui dépendent de ce signal et les met à jour —
// sans Virtual DOM, sans diff complet : seulement ce qui change.
//
// `i32` = entier signé 32 bits (le type entier par défaut en Rust)
let compteur = RwSignal::new(0i32);
// `Memo<T>` est une valeur calculée à partir d'un ou plusieurs signals.
// Elle se recalcule automatiquement quand `compteur` change, mais
// ne notifie ses dépendants que si sa valeur a effectivement changé.
//
// `move |_|` : une closure qui capture `compteur` par déplacement (ownership).
// - `move` : la closure prend possession de `compteur`
// - `|_|` : elle ignore son argument (la valeur précédente du memo)
let double = Memo::new(move |_| compteur.get() * 2);
view! {
<div class="page-accueil">
<h1>"Rust IPAM"</h1>
<p class="sous-titre">"Gestionnaire d'adresses IP"</p>
// --- Démonstration de la réactivité ---
// Dans un vrai projet IPAM, on afficherait ici la liste des sous-réseaux,
// les adresses disponibles, les statistiques d'utilisation...
<section class="demo-reactive">
<h2>"Réactivité Leptos"</h2>
// `{compteur}` insère la valeur du signal directement dans le DOM.
// Leptos met à jour UNIQUEMENT ce nœud texte quand compteur change.
// Pas de re-render du composant entier : c'est granulaire et efficace.
<p>"Compteur : " {compteur}</p>
// `{double}` : idem, mais pour le memo (valeur dérivée)
<p>"Double : " {double}</p>
<div class="boutons">
// `on:click` attache un event listener au bouton.
//
// `.update(|n| *n += 1)` :
// - prend une closure qui reçoit une référence mutable `&mut i32`
// - `*n` déréférence le pointeur pour modifier la valeur pointée
// - `+= 1` incrémente la valeur en place
<button on:click=move |_| compteur.update(|n| *n += 1)>
"+"
</button>
<button on:click=move |_| compteur.update(|n| *n -= 1)>
"-"
</button>
// `.set(0)` remplace directement la valeur (plus simple qu'update ici)
<button on:click=move |_| compteur.set(0)>
"Réinitialiser"
</button>
</div>
</section>
</div>
}
}

12
src/client/mod.rs Normal file
View File

@@ -0,0 +1,12 @@
// client/mod.rs — Module client (composants UI)
//
// Contient les pages et composants Leptos de l'application.
//
// Important : malgré le nom "client", ce code s'exécute des DEUX côtés :
// - Côté serveur : pour générer le HTML initial (SSR)
// - Côté navigateur : compilé en WASM pour rendre l'interface interactive
//
// Ne pas mettre ici de code qui nécessite des APIs navigateur (window, document...)
// sans le protéger avec `#[cfg(target_arch = "wasm32")]`.
pub mod home; // Page d'accueil

40
src/lib.rs Normal file
View File

@@ -0,0 +1,40 @@
// lib.rs — Racine de la bibliothèque partagée
//
// Ce fichier est compilé dans les DEUX modes :
// "ssr" → le serveur Axum l'utilise pour rendre du HTML
// "hydrate" → trunk le compile en WebAssembly pour le navigateur
//
// C'est ce partage de code qui rend Leptos "full-stack" :
// on écrit les composants une fois, ils s'exécutent des deux côtés.
// Déclaration des sous-modules de cette bibliothèque.
// `pub` les rend accessibles depuis main.rs et d'autres crates.
pub mod app; // Composant racine App() et configuration du routeur
pub mod client; // Pages et composants de l'interface utilisateur
pub mod server; // Handlers HTTP et logique métier côté serveur
// Point d'entrée WebAssembly — exécuté par le navigateur au chargement du bundle .wasm
//
// `#[cfg(feature = "hydrate")]` : ce code n'existe que dans le bundle WASM.
// `#[wasm_bindgen(start)]` : demande à wasm-bindgen d'appeler cette fonction
// automatiquement sans intervention JavaScript.
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn hydrate() {
use crate::app::App;
// Active les messages d'erreur Rust dans la console du navigateur.
// Sans ça, un panic Rust en WASM affiche juste "unreachable executed" — inutile.
// `set_once()` garantit qu'on ne l'initialise pas plusieurs fois si hydrate() est appelé plusieurs fois.
console_error_panic_hook::set_once();
// Monte l'application Leptos dans le <body> de la page HTML.
//
// En mode "hydration" (SSR + WASM), Leptos ne recrée pas le DOM depuis zéro.
// Il trouve le HTML déjà rendu par le serveur et y attache les event listeners
// pour rendre l'interface interactive. C'est plus rapide qu'un SPA classique
// qui construit tout le DOM côté client.
//
// `hydrate_body` (Leptos 0.7) = mode SSR + hydration (≠ `mount_to_body` qui repart de zéro)
leptos::mount::hydrate_body(App);
}

View File

@@ -1,3 +1,86 @@
fn main() { // main.rs — Point d'entrée du serveur Axum
println!("Hello, world!"); //
// Ce fichier est compilé UNIQUEMENT en mode "ssr" (Server-Side Rendering).
// `#[cfg(feature = "ssr")]` est l'équivalent d'un `#ifdef` en C :
// le code qu'il protège n'existe pas dans le bundle WASM.
//
// Pour lancer le serveur :
// cargo run --features ssr
//
// Pour lancer avec des logs détaillés :
// RUST_LOG=debug cargo run --features ssr
#[cfg(feature = "ssr")]
#[tokio::main]
// `#[tokio::main]` est une macro qui transforme notre `fn main()` synchrone
// en une fonction asynchrone, gérée par le runtime Tokio.
// Sans ça, Rust ne saurait pas comment exécuter du code `async`.
async fn main() {
use axum::Router;
use leptos::config::get_configuration;
use leptos_axum::{generate_route_list, LeptosRoutes};
use rust_ipam::{app::App, server::routes::not_found_handler};
use tower_http::services::ServeDir;
// Initialise le système de logs structurés.
// Les macros tracing::info!(), tracing::warn!(), tracing::error!()
// n'affichent rien sans cet initialisateur.
tracing_subscriber::fmt()
.with_env_filter(
// Lire le niveau de log depuis la variable d'environnement RUST_LOG,
// ou utiliser "info" par défaut si elle n'est pas définie.
// `unwrap_or_else` est une alternative idiomatique à `unwrap()` :
// elle fournit une valeur de repli au lieu de paniquer.
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
)
.init();
tracing::info!("Démarrage du serveur Rust IPAM...");
// Charge la configuration Leptos.
// Leptos peut lire un fichier `Leptos.toml` ou utiliser des valeurs par défaut.
// On utilise `.expect()` car le serveur ne peut pas fonctionner sans configuration.
// La chaîne passée à expect() est affichée si la valeur est Err ou None.
// Note : get_configuration() est synchrone en Leptos 0.7 — pas de .await ici.
let conf = get_configuration(None)
.expect("Impossible de charger la configuration Leptos");
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
// Analyse statiquement tous les composants `<Route>` dans `App`
// pour construire la liste des URLs que Leptos doit gérer.
let routes = generate_route_list(App);
// Construit le routeur Axum avec le pattern builder.
// Chaque méthode retourne un nouveau Router modifié — c'est du chaînage fonctionnel.
let app = Router::new()
// Sert les fichiers statiques compilés par trunk (WASM, CSS, JS...).
// `trunk build` les place dans `target/site/pkg/`.
// Les navigateurs les demandent via des URLs comme `/pkg/rust-ipam.wasm`.
.nest_service("/pkg", ServeDir::new("target/site/pkg"))
// Branche toutes les routes Leptos dans Axum.
// Pour chaque URL dans `routes`, Axum rend le composant App() en HTML.
.leptos_routes(&leptos_options, routes, App)
// Handler de repli : toute URL non reconnue retourne une 404.
.fallback(not_found_handler)
// Partage les options Leptos avec tous les handlers via le système d'état Axum.
.with_state(leptos_options);
// Crée un listener TCP sur l'adresse configurée (par défaut 127.0.0.1:3000).
let listener = tokio::net::TcpListener::bind(&addr)
.await
.expect(&format!("Impossible d'écouter sur l'adresse {}", addr));
tracing::info!("Serveur disponible sur http://{}", addr);
// Lance le serveur. Cette ligne bloque jusqu'à un Ctrl+C.
axum::serve(listener, app)
.await
.expect("Erreur critique du serveur");
} }
// Ce bloc vide est nécessaire pour que le compilateur trouve un `fn main()`
// quand on compile en mode WASM (où la feature "ssr" n'est pas activée).
// En WASM, le vrai point d'entrée est la fonction `hydrate()` dans lib.rs.
#[cfg(not(feature = "ssr"))]
fn main() {}

12
src/server/mod.rs Normal file
View File

@@ -0,0 +1,12 @@
// server/mod.rs — Module serveur
//
// Contient tout le code qui s'exécute uniquement côté serveur :
// - Handlers HTTP additionnels (routes.rs)
// - Fonctions serveur Leptos : accès à la base de données, authentification...
// - Logique métier qui ne doit JAMAIS être exposée dans le bundle WASM
//
// Les "server functions" Leptos (marquées #[server]) sont déclarées ici.
// Elles apparaissent comme des appels asynchrones normaux côté client,
// mais s'exécutent exclusivement sur le serveur — transparence totale.
pub mod routes;

26
src/server/routes.rs Normal file
View File

@@ -0,0 +1,26 @@
// server/routes.rs — Handlers HTTP Axum additionnels
//
// Ces handlers complètent les routes gérées par Leptos.
// Exemples d'usages futurs :
// - Endpoints API REST (/api/...)
// - Exports de fichiers (CSV, PDF...)
// - Webhooks entrants
// - Health check pour le monitoring (/health)
//
// `#[cfg(feature = "ssr")]` protège tout ce fichier :
// Axum n'existe pas dans le bundle WASM, donc on ne compile ce code qu'en mode serveur.
#[cfg(feature = "ssr")]
use axum::{http::StatusCode, response::IntoResponse};
// Handler 404 — utilisé comme fallback dans main.rs pour toute URL non reconnue.
//
// `impl IntoResponse` : Axum accepte n'importe quel type qui implémente ce trait.
// Un tuple `(StatusCode, &str)` l'implémente automatiquement :
// Axum en fait une réponse HTTP 404 avec le corps "Page introuvable".
//
// `async fn` est obligatoire pour les handlers Axum, même sans opération asynchrone.
#[cfg(feature = "ssr")]
pub async fn not_found_handler() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "Page introuvable")
}