# rust-cf-leptos Files: - rust-cf-leptos/.gitignore - rust-cf-leptos/Cargo.toml - rust-cf-leptos/README.md - rust-cf-leptos/crates/app/Cargo.toml - rust-cf-leptos/crates/app/src/lib.rs - rust-cf-leptos/crates/client/Cargo.toml - rust-cf-leptos/crates/client/src/lib.rs - rust-cf-leptos/crates/worker/Cargo.toml - rust-cf-leptos/crates/worker/src/lib.rs - rust-cf-leptos/e2e-tests/Cargo.toml - rust-cf-leptos/e2e-tests/src/main.rs - rust-cf-leptos/flake.nix - rust-cf-leptos/leptosfmt.toml - rust-cf-leptos/nix/run-e2e-tests.sh - rust-cf-leptos/nix/worker-entrypoint.js - rust-cf-leptos/rust-toolchain.toml - rust-cf-leptos/wrangler.toml - .github/workflows/rust-cf-leptos.yml --- ## File: rust-cf-leptos/.gitignore ``` # Build output target/ # Wrangler build artifacts .wrangler/ # Nix build symlinks result result-* # Node.js artifacts (wrangler dev dependency) node_modules/ # Log files *.log ``` ## File: rust-cf-leptos/Cargo.toml ``` [workspace] resolver = "2" members = ["crates/app", "crates/client", "crates/worker", "e2e-tests"] [workspace.package] edition = "2024" version = "0.1.0" [workspace.metadata.crane] name = "rust-cf-leptos" [workspace.dependencies] axum = { version = "0.8.7", default-features = false, features = [ "http1", "macros", ] } console_error_panic_hook = "0.1" console_log = "1" getrandom = { version = "0.3", features = ["wasm_js"] } leptos = { version = "0.8.13", default-features = false } leptos_axum = { version = "0.8.7", default-features = false, features = [ "wasm", ] } leptos_config = { version = "0.8.8" } leptos_meta = { version = "0.8.5", default-features = false } leptos_router = { version = "0.8.10", default-features = false } log = "0.4" server_fn = { version = "0.8.8", default-features = false } tower-service = "0.3" wasm-bindgen = "0.2" worker = "0.7.1" [profile.release] opt-level = "z" lto = "fat" codegen-units = 1 strip = true panic = "abort" ``` ## File: rust-cf-leptos/README.md ``` # rust-cf-leptos A Rust web application using Leptos for server-side rendering, deployed to Cloudflare Workers. Demonstrates best practices from [AGENTS.md](../AGENTS.md). ## Architecture ``` crates/ app/ # Shared Leptos components (isomorphic) client/ # Client-side hydration (compiles to WASM) worker/ # Cloudflare Worker server (compiles to WASM) e2e-tests/ # End-to-end tests with Selenium/Firefox ``` Both the server (worker) and client compile to WebAssembly. The worker handles SSR and serves static assets, while the client hydrates the page for interactivity. ## Design Guidelines Compliance ### Pinned Dependencies - **Nix**: Uses `flake.nix` with pinned nixpkgs (`nixos-25.05`) for reproducible builds - **Rust toolchain**: Pins exact version (`1.91.0`) in `rust-toolchain.toml` - **CVE database**: Pins `advisory-db` for consistent security audits - **Lock file**: Commits `Cargo.lock` for deterministic builds - **wasm-bindgen**: Pins exact version (`0.2.105`) to match CLI and library ### Automatic Linting All linters run via `nix flake check`: - **cargo clippy**: Rust linting with `--deny warnings` - **statix**: Nix static analysis - **shellcheck**: Shell script linting - **cargo-audit**: Security vulnerability scanning ### Code Formatting Formatting enforced in CI: - **rustfmt**: Rust code formatting - **taplo**: `Cargo.toml` formatting - **alejandra**: Nix file formatting - **shfmt**: Shell script formatting ## Usage ```bash # Enter development shell nix develop # Build the website (worker + client bundles) nix build .#website # Run all checks (lint, format, audit) nix flake check # Run end-to-end tests nix run .#e2e-tests # Deploy to Cloudflare (requires wrangler auth) wrangler deploy ``` ## Development The `nix develop` shell provides all tools needed for local development: ```bash # Start local dev server wrangler dev # Run clippy manually cargo clippy --all-targets -- -D warnings # Format code cargo fmt alejandra . shfmt -w nix/*.sh ``` ``` ## File: rust-cf-leptos/crates/app/Cargo.toml ``` [package] name = "app" version.workspace = true edition.workspace = true publish = false [lib] path = "src/lib.rs" crate-type = ["cdylib", "rlib"] [dependencies] leptos = { workspace = true } leptos_router = { workspace = true } leptos_config = { workspace = true } leptos_meta = { workspace = true } server_fn = { workspace = true } [features] default = [] hydrate = ["leptos/hydrate"] ssr = ["leptos/ssr", "server_fn/ssr"] ``` ## File: rust-cf-leptos/crates/app/src/lib.rs ``` use leptos::{ hydration::{AutoReload, HydrationScripts}, prelude::*, }; use leptos_config::LeptosOptions; use leptos_meta::{MetaTags, provide_meta_context}; use leptos_router::{ components::{Route, Router, Routes}, path, }; /// A simple server function that adds two numbers on the server. /// This demonstrates server-side computation with Leptos server functions. #[server(AddNumbers, "/api")] pub async fn add_numbers(a: i32, b: i32) -> Result { // This code only runs on the server Ok(a + b) } #[component] fn Home() -> impl IntoView { let (count, set_count) = signal(0); let increment = move |_| *set_count.write() += 1; // Server function action let add_action = ServerAction::::new(); let result = add_action.value(); view! {

"Leptos on Cloudflare"

"Minimal Leptos skeleton rendered server-side on Cloudflare Workers."

"Refresh"
// Server function demo section

"Server Function Demo"

"Click the button to compute 2 + 3 on the server."

"Result: " {move || match result.get() { Some(Ok(val)) => val.to_string(), Some(Err(e)) => format!("Error: {}", e), None => "—".to_string(), }}

} } #[component] pub fn App() -> impl IntoView { provide_meta_context(); view! {
} } pub fn shell(options: LeptosOptions) -> impl IntoView { view! { "Leptos on Cloudflare" } } ``` ## File: rust-cf-leptos/crates/client/Cargo.toml ``` [package] name = "client" version.workspace = true edition.workspace = true publish = false [lib] path = "src/lib.rs" crate-type = ["cdylib", "rlib"] [dependencies] console_error_panic_hook = { workspace = true } console_log = { workspace = true } app = { path = "../app", features = ["hydrate"] } leptos = { workspace = true, features = ["hydrate"] } log = { workspace = true } wasm-bindgen = { workspace = true } ``` ## File: rust-cf-leptos/crates/client/src/lib.rs ``` use app::App; use leptos::mount; use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen] pub fn hydrate() { _ = console_log::init_with_level(log::Level::Debug); console_error_panic_hook::set_once(); mount::hydrate_body(App); } ``` ## File: rust-cf-leptos/crates/worker/Cargo.toml ``` [package] name = "worker" version.workspace = true edition.workspace = true publish = false [lib] path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] app = { path = "../app", features = ["ssr"] } axum = { workspace = true } console_error_panic_hook = { workspace = true } console_log = { workspace = true } getrandom = { workspace = true } leptos = { workspace = true, features = ["ssr"] } leptos_axum = { workspace = true } leptos_config = { workspace = true } log = { workspace = true } server_fn = { workspace = true, features = ["ssr", "axum-no-default"] } tower-service = { workspace = true } worker = { workspace = true, features = ["http"] } ``` ## File: rust-cf-leptos/crates/worker/src/lib.rs ``` #![allow(non_snake_case)] use app::{App, shell}; use axum::{Extension, Router, routing::post}; use leptos_axum::{LeptosRoutes, generate_route_list, handle_server_fns_with_context}; use leptos_config::LeptosOptions; use std::sync::Arc; use tower_service::Service; use worker::{Context, Env, HttpRequest, Result, event}; /// Register server functions at worker start. /// This must be called before any server functions can be handled. #[event(start)] fn register() { server_fn::axum::register_explicit::(); } fn router(env: Env) -> Router<()> { let leptos_options = LeptosOptions::builder() .output_name("client") .site_pkg_dir("pkg") .build(); let routes = generate_route_list(App); for route in routes.iter() { log::info!("Registering Leptos route {}", route.path()); } Router::new() // Handle server function requests at /api/* .route( "/api/{*fn_name}", post(|req| handle_server_fns_with_context(|| {}, req)), ) .leptos_routes(&leptos_options, routes, { let leptos_options = leptos_options.clone(); move || shell(leptos_options.clone()) }) .with_state(leptos_options) .layer(Extension(Arc::new(env))) } #[event(fetch)] pub async fn fetch( req: HttpRequest, env: Env, _ctx: Context, ) -> Result> { _ = console_log::init_with_level(log::Level::Debug); console_error_panic_hook::set_once(); log::info!("fetch called for {} {}", req.method(), req.uri().path()); Ok(router(env).call(req).await?) } ``` ## File: rust-cf-leptos/e2e-tests/Cargo.toml ``` [package] name = "e2e-tests" version = "0.1.0" edition = "2024" publish = false [dependencies] anyhow = "1" thirtyfour = "0.36.1" tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } ``` ## File: rust-cf-leptos/e2e-tests/src/main.rs ``` use anyhow::{Context, Result}; use std::time::Duration; use thirtyfour::prelude::*; struct TestRunner { driver: WebDriver, base_url: String, } impl TestRunner { async fn new() -> Result { let base_url = std::env::var("E2E_BASE_URL").unwrap_or_else(|_| "http://127.0.0.1:8787".to_string()); let webdriver_port = std::env::var("WEBDRIVER_PORT").unwrap_or_else(|_| "4444".to_string()); let mut caps = DesiredCapabilities::firefox(); caps.set_headless()?; let driver = WebDriver::new(&format!("http://localhost:{}", webdriver_port), caps) .await .context("creating WebDriver connection")?; driver .set_implicit_wait_timeout(Duration::from_secs(10)) .await?; Ok(Self { driver, base_url }) } async fn get_page_source(&self) -> Result { self.driver.goto(&self.base_url).await?; self.driver.source().await.context("getting page source") } async fn get_css_content(&self) -> Result { let css_url = format!("{}/pkg/styles.css", self.base_url); self.driver.goto(&css_url).await?; self.driver.source().await.context("getting CSS content") } } impl TestRunner { async fn quit(self) -> Result<()> { self.driver.quit().await.context("quitting WebDriver") } } // ============================================================================ // Test Definitions // ============================================================================ /// Test: Main page is reachable and contains expected content async fn test_main_page_reachable(runner: &TestRunner) -> Result<()> { let body = runner.get_page_source().await?; assert!( body.contains("Leptos on Cloudflare"), "HTML should contain 'Leptos on Cloudflare'" ); Ok(()) } /// Test: CSS stylesheet link is present in HTML head async fn test_css_link_present(runner: &TestRunner) -> Result<()> { let body = runner.get_page_source().await?; assert!( body.contains(r#"href="/pkg/styles.css""#) && body.contains("stylesheet"), "HTML should contain CSS link tag with /pkg/styles.css" ); Ok(()) } /// Test: CSS file is accessible and not empty async fn test_css_file_accessible(runner: &TestRunner) -> Result<()> { let css_content = runner.get_css_content().await?; assert!(!css_content.is_empty(), "CSS file should not be empty"); assert!( css_content.len() >= 100, "CSS file should have sufficient content (at least 100 bytes, got {})", css_content.len() ); Ok(()) } /// Test: CSS contains required Tailwind utility classes async fn test_css_contains_tailwind_classes(runner: &TestRunner) -> Result<()> { let css_content = runner.get_css_content().await?; // Classes that should be present based on the HTML structure let required_classes = [ (".mx-auto", "section element"), (".flex", "div container"), (".items-center", "flex container"), (".justify-center", "flex container"), (".text-center", "section element"), (".min-h-screen", "main element"), ]; let mut missing = Vec::new(); for (class, context) in &required_classes { if !css_content.contains(class) { missing.push(format!("{} (used in {})", class, context)); } } assert!( missing.is_empty(), "CSS should contain all required Tailwind classes. Missing: {:?}", missing ); Ok(()) } /// Test: CSS is valid Tailwind CSS output async fn test_css_is_valid_tailwind(runner: &TestRunner) -> Result<()> { let css_content = runner.get_css_content().await?; assert!( css_content.contains("tailwindcss"), "CSS should contain Tailwind CSS identifier" ); Ok(()) } /// Test: Server function demo section is present async fn test_server_function_section_present(runner: &TestRunner) -> Result<()> { let body = runner.get_page_source().await?; assert!( body.contains("Server Function Demo"), "HTML should contain 'Server Function Demo' section" ); assert!( body.contains("Calculate 2 + 3"), "HTML should contain the server function button" ); Ok(()) } /// Test: Server function executes and returns correct result async fn test_server_function_executes(runner: &TestRunner) -> Result<()> { runner.driver.goto(&runner.base_url).await?; // Find and click the "Calculate 2 + 3" button let button = runner .driver .find(By::XPath("//button[contains(text(), 'Calculate 2 + 3')]")) .await .context("finding server function button")?; button .click() .await .context("clicking server function button")?; // Wait for the result to appear (the server function should return 5) tokio::time::sleep(Duration::from_millis(500)).await; // Check that the result shows "5" let result_element = runner .driver .find(By::Id("server-result")) .await .context("finding server result element")?; let result_text = result_element.text().await.context("getting result text")?; assert!( result_text.contains('5'), "Server function should return 5 for 2 + 3, but got: {}", result_text ); Ok(()) } // ============================================================================ // Test Runner // ============================================================================ macro_rules! run_tests { ($runner:expr; $( $name:literal => $test:ident ),* $(,)? ) => {{ let test_names: &[&str] = &[$($name),*]; let total = test_names.len(); println!("Running {} tests...\n", total); let mut idx = 0; $( idx += 1; print!("[{}/{}] {} ... ", idx, total, $name); match $test($runner).await { Ok(()) => println!("āœ…"), Err(e) => { println!("āŒ"); anyhow::bail!("Test '{}' failed: {}", $name, e); } } )* println!("\nāœ… All {} tests passed!", total); Ok::<(), anyhow::Error>(()) }}; } #[tokio::main] async fn main() -> Result<()> { let runner = TestRunner::new().await?; let result = run_tests!(&runner; "Main page is reachable" => test_main_page_reachable, "CSS link present in HTML" => test_css_link_present, "CSS file is accessible" => test_css_file_accessible, "CSS contains Tailwind classes" => test_css_contains_tailwind_classes, "CSS is valid Tailwind output" => test_css_is_valid_tailwind, "Server function section present" => test_server_function_section_present, "Server function executes correctly" => test_server_function_executes, ); // Explicitly quit WebDriver to avoid Tokio runtime shutdown panic runner.quit().await?; result } ``` ## File: rust-cf-leptos/flake.nix ``` { description = "Rust + Leptos + Cloudflare Workers example"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; flake-utils.url = "github:numtide/flake-utils"; rust-overlay.url = "github:oxalica/rust-overlay"; crane.url = "github:ipetkov/crane"; wrangler = { url = "github:emrldnix/wrangler"; inputs.nixpkgs.follows = "nixpkgs"; }; alejandra = { url = "github:kamadorueda/alejandra"; inputs.nixpkgs.follows = "nixpkgs"; }; advisory-db = { url = "github:rustsec/advisory-db"; flake = false; }; }; outputs = { self, nixpkgs, flake-utils, rust-overlay, crane, wrangler, alejandra, advisory-db, ... }: flake-utils.lib.eachDefaultSystem (system: let overlays = [ (import rust-overlay) ]; pkgs = import nixpkgs {inherit system overlays;}; inherit (pkgs) lib; fakeHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; wasmToolchain = rustToolchain.override { targets = ["wasm32-unknown-unknown"]; }; craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; craneLibWasm = (crane.mkLib pkgs).overrideToolchain wasmToolchain; # Configure build parallelism to reduce disk/memory pressure in CI buildJobsEnv = pkgs.lib.optionalAttrs (builtins.getEnv "CI" != "") { CARGO_BUILD_JOBS = "2"; }; wranglerPkg = wrangler.packages.${system}.default; wasmBindgenVersion = "0.2.106"; wasmBindgenCli = pkgs.rustPlatform.buildRustPackage { pname = "wasm-bindgen-cli"; version = wasmBindgenVersion; src = pkgs.fetchCrate { pname = "wasm-bindgen-cli"; version = wasmBindgenVersion; sha256 = "sha256-M6WuGl7EruNopHZbqBpucu4RWz44/MSdv6f0zkYw+44="; }; cargoHash = "sha256-ElDatyOwdKwHg3bNH/1pcxKI7LXkhsotlDPQjiLHBwA="; }; src = craneLib.cleanCargoSource (craneLib.path ./.); # Base args for WASM packages (app, client, worker) # Note: use worker@0.1.0 to disambiguate from the worker crate dependency wasmBaseArgs = { inherit src; strictDeps = true; cargoExtraArgs = "--locked --package app --package client --package worker@0.1.0"; cargoHash = fakeHash; cargoLock = ./Cargo.lock; CARGO_BUILD_TARGET = "wasm32-unknown-unknown"; pname = "wasm-packages"; }; clientArgs = wasmBaseArgs // { pname = "client"; cargoExtraArgs = "--locked --package client"; }; workerArgs = wasmBaseArgs // { pname = "worker"; cargoExtraArgs = "--locked --package worker@0.1.0"; }; e2eArgs = { inherit src; strictDeps = true; pname = "e2e-tests"; cargoExtraArgs = "--locked --package e2e-tests"; cargoHash = fakeHash; cargoLock = ./Cargo.lock; } // buildJobsEnv; clientArtifacts = craneLibWasm.buildDepsOnly clientArgs; workerArtifacts = craneLibWasm.buildDepsOnly workerArgs; e2eTests = craneLib.buildPackage (e2eArgs // { cargoArtifacts = craneLib.buildDepsOnly e2eArgs; doCheck = false; }); clientBundle = craneLibWasm.buildPackage (clientArgs // { cargoArtifacts = clientArtifacts; doCheck = false; nativeBuildInputs = [ wasmBindgenCli pkgs.binaryen ]; installPhase = '' runHook preInstall client_wasm="target/wasm32-unknown-unknown/release/client.wasm" out_pkg="$out/assets/pkg" mkdir -p "$out_pkg" wasm-bindgen "$client_wasm" \ --out-dir "$out_pkg" \ --out-name client \ --target web \ --no-typescript wasm-opt \ "$out_pkg/client_bg.wasm" \ -o "$out_pkg/client_bg.wasm" \ -Oz \ --enable-bulk-memory \ --enable-mutable-globals \ --enable-sign-ext \ --enable-nontrapping-float-to-int runHook postInstall ''; }); workerBundle = craneLibWasm.buildPackage (workerArgs // { cargoArtifacts = workerArtifacts; doCheck = false; nativeBuildInputs = [ wasmBindgenCli pkgs.binaryen ]; installPhase = '' runHook preInstall worker_wasm="target/wasm32-unknown-unknown/release/worker.wasm" out_worker="$out/worker" mkdir -p "$out_worker" wasm-bindgen "$worker_wasm" \ --out-dir "$out_worker" \ --out-name index \ --target web \ --no-typescript wasm-opt \ "$out_worker/index_bg.wasm" \ -o "$out_worker/index_bg.wasm" \ -Oz \ --enable-bulk-memory \ --enable-mutable-globals \ --enable-sign-ext \ --enable-nontrapping-float-to-int install -Dm644 ${./nix/worker-entrypoint.js} "$out_worker/worker.js" runHook postInstall ''; }); inputCssFile = pkgs.writeText "input.css" '' @tailwind base; @tailwind components; @tailwind utilities; ''; tailwindConfigFile = pkgs.writeText "tailwind.config.js" '' /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./crates/**/*.rs", ], theme: { extend: {}, }, plugins: [], } ''; tailwindCss = pkgs.stdenv.mkDerivation { pname = "tailwindcss"; version = "0.1.0"; src = null; dontUnpack = true; nativeBuildInputs = [ pkgs.tailwindcss_4 pkgs.nodejs_20 ]; buildPhase = '' runHook preBuild # Create a temporary directory with the files we need BUILD_DIR="$PWD/build-dir" mkdir -p "$BUILD_DIR" cd "$BUILD_DIR" # Copy input.css and config cp ${inputCssFile} input.css cp ${tailwindConfigFile} tailwind.config.js # Copy crates directory for content scanning cp -r ${lib.cleanSource ./crates} crates # Build CSS tailwindcss -i input.css -o styles.css --minify # Move back to original directory and save path cd "$OLDPWD" echo "$BUILD_DIR/styles.css" > styles-css-path.txt runHook postBuild ''; installPhase = '' runHook preInstall mkdir -p $out BUILD_DIR="$(pwd)/build-dir" if [ -f "$BUILD_DIR/styles.css" ]; then install -Dm644 "$BUILD_DIR/styles.css" $out/styles.css else echo "Error: styles.css not found in $BUILD_DIR" find . -name "styles.css" || true exit 1 fi runHook postInstall ''; meta = with lib; { description = "Compiled Tailwind CSS"; platforms = platforms.all; }; }; website = pkgs.stdenv.mkDerivation { pname = "website"; version = "0.1.0"; src = null; dontUnpack = true; installPhase = '' runHook preInstall mkdir -p $out # Copy CSS first to ensure directory is writable install -Dm644 ${tailwindCss}/styles.css $out/assets/pkg/styles.css # Then copy other assets (this will overwrite if needed, but CSS is already there) cp -r ${clientBundle}/assets/* $out/assets/ || true cp -r ${workerBundle}/worker $out/ # Ensure CSS is still there after copying assets cp ${tailwindCss}/styles.css $out/assets/pkg/styles.css runHook postInstall ''; meta = with lib; { description = "Worker bundle with client assets"; platforms = platforms.all; }; }; webappPath = website; e2eBinPath = e2eTests; in { devShells.default = pkgs.mkShell { packages = with pkgs; [ # Rust toolchain cargo rustc rustfmt clippy # WASM tools wasm-pack binaryen # Cloudflare tools nodejs_20 wranglerPkg # Testing tools geckodriver # Linting tools shellcheck statix cargo-audit # Formatting tools alejandra.packages.${system}.default leptosfmt shfmt taplo ]; RUSTFLAGS = "-C target-feature=+simd128"; }; packages.e2e-tests = e2eTests; packages.website = website; apps.e2e-tests = flake-utils.lib.mkApp { drv = pkgs.writeShellApplication { name = "e2e-tests"; runtimeInputs = with pkgs; [geckodriver firefox curl wranglerPkg]; text = '' WEBAPP_PATH=${webappPath} CURL_BIN=${pkgs.curl}/bin/curl WRANGLER_BIN=${wranglerPkg}/bin/wrangler E2E_TESTS_BIN=${e2eBinPath}/bin/e2e-tests export WEBAPP_PATH CURL_BIN WRANGLER_BIN E2E_TESTS_BIN ${./nix/run-e2e-tests.sh} ''; }; }; checks = let nixSrc = lib.sources.sourceFilesBySuffices ./. [".nix"]; shellSrc = lib.sources.sourceFilesBySuffices ./nix [".sh"]; in { # Nix formatting with alejandra alejandra = pkgs.runCommand "alejandra-check" {} '' ${alejandra.packages.${system}.default}/bin/alejandra --check ${nixSrc} touch $out ''; # Nix linting with statix statix = pkgs.runCommand "statix-check" {buildInputs = [pkgs.statix];} '' statix check ${nixSrc} touch $out ''; # Shell script linting with shellcheck shellcheck = pkgs.runCommand "shellcheck" {buildInputs = [pkgs.shellcheck];} '' shellcheck ${shellSrc}/*.sh touch $out ''; # Shell script formatting with shfmt shfmt = pkgs.runCommand "shfmt-check" {buildInputs = [pkgs.shfmt];} '' shfmt -d ${shellSrc}/*.sh touch $out ''; # Rust clippy linting (WASM packages only - excludes e2e-tests) clippy = craneLibWasm.cargoClippy (wasmBaseArgs // { pname = "clippy-wasm"; cargoArtifacts = craneLibWasm.buildDepsOnly wasmBaseArgs; cargoClippyExtraArgs = "--package app --package client --package worker@0.1.0 -- --deny warnings"; }); # Rust formatting rustfmt = craneLib.cargoFmt {inherit src;}; # Cargo.toml formatting taplo = craneLib.taploFmt {inherit src;}; # Leptos view! macro formatting leptosfmt = pkgs.runCommand "leptosfmt-check" {buildInputs = [pkgs.leptosfmt];} '' leptosfmt --config-file ${./leptosfmt.toml} --check ${src}/crates touch $out ''; # Security audit audit = craneLib.cargoAudit { inherit advisory-db src; }; }; }); } ``` ## File: rust-cf-leptos/leptosfmt.toml ``` max_width = 120 tab_spaces = 2 ``` ## File: rust-cf-leptos/nix/run-e2e-tests.sh ``` #!/usr/bin/env bash set -e # This script runs end-to-end tests with geckodriver and wrangler # Expected environment variables: # WEBAPP_PATH - Path to the built webapp # CURL_BIN - Path to curl binary # WRANGLER_BIN - Path to wrangler binary # E2E_TESTS_BIN - Path to e2e_tests binary # WRANGLER_PORT (optional, default: 8787) # WEBDRIVER_PORT (optional, default: 4444) # E2E_BROWSER (optional, default: firefox) # shellcheck disable=SC2153 : "${WEBAPP_PATH:?}" "${CURL_BIN:?}" "${WRANGLER_BIN:?}" "${E2E_TESTS_BIN:?}" # Create a temporary directory for all e2e test artifacts # This prevents polluting the user's CWD with node_modules, logs, etc. E2E_TMPDIR=$(mktemp -d) echo "Using temp directory: $E2E_TMPDIR" # Setup cleanup trap to kill wrangler and geckodriver on exit # shellcheck disable=SC2317,SC2329 cleanup() { local exit_code=${TEST_EXIT_CODE:-1} if [ -n "${WRANGLER_PID:-}" ]; then echo "Stopping wrangler and its child processes..." # Kill all children of wrangler first pkill -P "$WRANGLER_PID" 2>/dev/null || true # Then kill wrangler itself kill "$WRANGLER_PID" 2>/dev/null || true # Wait a moment then force kill if still running sleep 1 kill -9 "$WRANGLER_PID" 2>/dev/null || true fi if [ -n "${GECKODRIVER_PID:-}" ]; then echo "Stopping geckodriver..." kill "$GECKODRIVER_PID" 2>/dev/null || true fi # Only keep logs on failure if [ "$exit_code" -ne 0 ] && [ -d "$E2E_TMPDIR" ]; then echo "Tests failed. Logs available in: $E2E_TMPDIR" echo "--- geckodriver.log ---" cat "$E2E_TMPDIR/geckodriver.log" 2>/dev/null || true echo "--- wrangler.log ---" cat "$E2E_TMPDIR/wrangler.log" 2>/dev/null || true else # Clean up temp directory on success rm -rf "$E2E_TMPDIR" fi } trap cleanup EXIT # Create result symlink inside temp directory # Wrangler config references result/ for e2e environment ln -sfn "$WEBAPP_PATH" "$E2E_TMPDIR/result" # Copy wrangler.toml to temp directory cp wrangler.toml "$E2E_TMPDIR/" # Start geckodriver in the background (logs to temp dir) echo "Starting geckodriver..." WEBDRIVER_PORT=${WEBDRIVER_PORT:-4444} geckodriver --port="$WEBDRIVER_PORT" >"$E2E_TMPDIR/geckodriver.log" 2>&1 & GECKODRIVER_PID=$! echo "Geckodriver started with PID $GECKODRIVER_PID on port $WEBDRIVER_PORT" # Wait for geckodriver to start (up to 10 seconds) echo "Waiting for geckodriver to be ready..." for i in {1..10}; do if "$CURL_BIN" -sf "http://localhost:$WEBDRIVER_PORT/status" >/dev/null 2>&1; then echo "Geckodriver is ready!" break fi if [ "$i" -eq 10 ]; then echo "Geckodriver failed to start within 10 seconds" echo "Last 20 lines of geckodriver.log:" tail -20 "$E2E_TMPDIR/geckodriver.log" || true exit 1 fi sleep 1 done # Start wrangler dev with e2e environment (runs in temp dir to contain node_modules) echo "Starting wrangler dev with e2e environment..." WRANGLER_PORT=${WRANGLER_PORT:-8787} (cd "$E2E_TMPDIR" && "$WRANGLER_BIN" dev --env e2e --port "$WRANGLER_PORT" >wrangler.log 2>&1) & WRANGLER_PID=$! echo "Wrangler started with PID $WRANGLER_PID" # Wait for wrangler to start (up to 30 seconds) echo "Waiting for wrangler to start on port $WRANGLER_PORT..." for i in {1..30}; do if "$CURL_BIN" -sf "http://localhost:$WRANGLER_PORT" >/dev/null 2>&1; then echo "Wrangler is ready!" break fi if [ "$i" -eq 30 ]; then echo "Wrangler failed to start within 30 seconds" echo "Last 20 lines of wrangler.log:" tail -20 "$E2E_TMPDIR/wrangler.log" || true exit 1 fi sleep 1 done # Run e2e tests with headless Firefox echo "Running e2e tests with headless Firefox..." export WRANGLER_PORT export WEBDRIVER_PORT export E2E_BROWSER=${E2E_BROWSER:-firefox} # Run tests and capture exit code "$E2E_TESTS_BIN" TEST_EXIT_CODE=$? # Exit with test exit code (cleanup trap will run automatically) exit $TEST_EXIT_CODE ``` ## File: rust-cf-leptos/nix/worker-entrypoint.js ``` // Cloudflare Worker entrypoint that lazy-loads the wasm-bindgen output. // Import the compiled wasm module directly so we can initialize without // relying on URL resolution within the Workers runtime. import init, { fetch as wasmFetch } from './index.js'; import wasmModule from './index_bg.wasm'; let initPromise; async function ensureInitialized() { if (!initPromise) { initPromise = init(wasmModule); } return initPromise; } export default { async fetch(request, env, ctx) { await ensureInitialized(); return wasmFetch(request, env, ctx); }, }; ``` ## File: rust-cf-leptos/rust-toolchain.toml ``` [toolchain] channel = "1.91.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] ``` ## File: rust-cf-leptos/wrangler.toml ``` name = "rust-cf-leptos" main = ".wrangler/dist/worker/worker.js" compatibility_date = "2024-11-23" workers_dev = true [build] command = "nix build .#website -o .wrangler/dist" [assets] directory = ".wrangler/dist/assets" binding = "ASSETS" not_found_handling = "single-page-application" [env.e2e] main = "result/worker/worker.js" [env.e2e.build] # Don't run build command - website is pre-built by Nix command = "" [env.e2e.assets] directory = "result/assets" binding = "ASSETS" not_found_handling = "single-page-application" ``` ## File: .github/workflows/rust-cf-leptos.yml ``` name: Rust CF Leptos CI on: push: branches: [main, master] paths: - "rust-cf-leptos/**" - ".github/workflows/rust-cf-leptos.yml" pull_request: paths: - "rust-cf-leptos/**" - ".github/workflows/rust-cf-leptos.yml" permissions: contents: read jobs: check: runs-on: ubuntu-24.04 defaults: run: working-directory: rust-cf-leptos steps: - uses: actions/checkout@v6 - name: Free disk space uses: jlumbroso/free-disk-space@v1.3.1 with: tool-cache: false android: true dotnet: true haskell: true large-packages: true docker-images: true swap-storage: false - name: Install Nix uses: DeterminateSystems/nix-installer-action@v21 - name: Configure Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@v13 - name: Run nix flake check run: nix flake check --print-build-logs - name: Run e2e tests run: nix run .#e2e-tests --print-build-logs ```