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

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() {
println!("Hello, world!");
// main.rs — Point d'entrée du serveur Axum
//
// 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")
}