Created
January 7, 2026 21:39
-
-
Save yurivish/3d365fd6657d07c4ff6b277e1d089b84 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Prototyping a live-reload workflow with auto-js-minification built-in. License is MIT: https://opensource.org/license/mit | |
| use axum::{ | |
| Router, | |
| extract::Path, | |
| http::{StatusCode, header}, | |
| response::IntoResponse, | |
| routing::get, | |
| }; | |
| use clap::Parser; | |
| use maud::{Markup, html}; | |
| use notify::Watcher; | |
| use std::path::PathBuf; | |
| use std::sync::Arc; | |
| use std::time::Duration; | |
| use tokio::net::TcpListener; | |
| use tower::ServiceBuilder; | |
| use tower_http::compression::CompressionLayer; | |
| use tower_http::decompression::RequestDecompressionLayer; | |
| use tower_http::services::{ServeDir, ServeFile}; | |
| use tower_livereload::LiveReloadLayer; | |
| #[derive(Parser, Debug, Clone)] | |
| #[command(author, version, about, long_about = None)] | |
| struct Config { | |
| /// Directory for serving static assets | |
| #[arg(long, default_value = "src/assets")] | |
| assets_dir: PathBuf, | |
| } | |
| fn compress_js(code: &str) -> Result<String, String> { | |
| use oxc_allocator::Allocator; | |
| use oxc_codegen::{Codegen, CodegenOptions}; | |
| use oxc_minifier::{MangleOptions, Minifier, MinifierOptions}; | |
| use oxc_parser::Parser; | |
| use oxc_span::SourceType; | |
| let allocator = Allocator::default(); | |
| let source_type = SourceType::ts(); | |
| let ret = Parser::new(&allocator, code, source_type).parse(); | |
| // Check for parse errors | |
| if !ret.errors.is_empty() { | |
| let error_messages: Vec<String> = ret.errors.iter().map(|e| format!("{:?}", e)).collect(); | |
| return Err(format!("Parse errors: {}", error_messages.join(", "))); | |
| } | |
| let mut program = ret.program; | |
| let options = MinifierOptions { | |
| mangle: Some(MangleOptions { | |
| top_level: false, | |
| ..Default::default() | |
| }), | |
| ..Default::default() | |
| }; | |
| let minifier = Minifier::new(options); | |
| minifier.minify(&allocator, &mut program); | |
| let codegen_options = CodegenOptions { | |
| minify: true, | |
| ..Default::default() | |
| }; | |
| let result = Codegen::new().with_options(codegen_options).build(&program); | |
| Ok(result.code) | |
| } | |
| #[tokio::main] | |
| async fn main() -> Result<(), Box<dyn std::error::Error>> { | |
| println!( | |
| "{:?}", | |
| compress_js("import { x } from './foo.js'; const x: number = 1 + 1; console.log(x);") | |
| ); | |
| if true { | |
| return Ok(()); | |
| } | |
| let config = Config::parse(); | |
| // The reload interval configures the SSE `retry` between disconnects | |
| let livereload = LiveReloadLayer::new().reload_interval(Duration::from_millis(200)); | |
| let state = AppState { config }; | |
| let reloader = livereload.reloader(); | |
| let mut watcher = notify::recommended_watcher(move |res: Result<notify::Event, _>| { | |
| if let Ok(event) = res { | |
| // Reload, unless it's just a read. | |
| if !matches!(event.kind, notify::EventKind::Access(_)) { | |
| reloader.reload(); | |
| } | |
| } | |
| }) | |
| .expect("failed to initialize watcher"); | |
| watcher | |
| .watch(&state.config.assets_dir, notify::RecursiveMode::Recursive) | |
| .expect("failed to watch assets folder"); | |
| let listener = TcpListener::bind("0.0.0.0:8080") | |
| .await | |
| .expect("cannot construct tokio::net::TcpListener"); | |
| println!("Listening on {}", listener.local_addr()?); | |
| let app = app(livereload, state); | |
| axum::serve(listener, app) | |
| .await | |
| .expect("failed to run http server"); | |
| Ok(()) | |
| } | |
| struct AppState { | |
| config: Config, | |
| } | |
| fn app(livereload: LiveReloadLayer, state: AppState) -> Router { | |
| let state = Arc::new(state); | |
| let assets_dir = &state.config.assets_dir; | |
| let index_path = assets_dir.join("index.html"); | |
| let router = Router::new() | |
| .route("/about", get(about)) | |
| .route("/js/{*path}", get(serve_js)) | |
| .with_state(state.clone()) | |
| .route_service("/", ServeDir::new(assets_dir)) | |
| .fallback_service(ServeFile::new(index_path)); | |
| router.layer( | |
| ServiceBuilder::new() | |
| .layer(RequestDecompressionLayer::new()) | |
| .layer(CompressionLayer::new()) | |
| .layer(livereload), | |
| ) | |
| } | |
| async fn serve_js(Path(path): Path<String>) -> impl IntoResponse { | |
| let source_path = format!("static/js/{path}"); | |
| match tokio::fs::read_to_string(&source_path).await { | |
| Ok(source) => { | |
| let minified = minify_js(&source); // your minification fn | |
| ( | |
| StatusCode::OK, | |
| [(header::CONTENT_TYPE, "application/javascript")], | |
| minified, | |
| ) | |
| } | |
| Err(_) => ( | |
| StatusCode::NOT_FOUND, | |
| [(header::CONTENT_TYPE, "text/plain")], | |
| "Not found".to_string(), | |
| ), | |
| } | |
| } | |
| async fn about() -> Markup { | |
| html! { | |
| h1 data-init="123" { "Hello, World!" } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment