Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: prefetch support

+167 -17
+53
crates/maudit/js/prefetch.ts
··· 1 + const preloadedResources = new Set<string>(); 2 + 3 + interface PreloadConfig { 4 + skipConnectionCheck?: boolean; 5 + } 6 + 7 + export function prefetch(url: string, config?: PreloadConfig) { 8 + url = url.replace(/#.*/, ""); 9 + 10 + const bypassConnectionCheck = config?.skipConnectionCheck ?? false; 11 + 12 + if (!canPrefetchUrl(url, bypassConnectionCheck)) { 13 + return; 14 + } 15 + 16 + const linkElement = document.createElement("link"); 17 + const supportsPrefetch = linkElement.relList?.supports?.("prefetch"); 18 + 19 + if (supportsPrefetch) { 20 + linkElement.rel = "prefetch"; 21 + linkElement.href = url; 22 + document.head.appendChild(linkElement); 23 + preloadedResources.add(url); 24 + } 25 + } 26 + 27 + function canPrefetchUrl(url: string, bypassConnectionCheck: boolean): boolean { 28 + if (!navigator.onLine) return false; 29 + if (!bypassConnectionCheck && hasLimitedBandwidth()) return false; 30 + 31 + try { 32 + const destination = new URL(url, window.location.href); 33 + 34 + return ( 35 + (window.location.origin === destination.origin && 36 + window.location.pathname !== destination.pathname) || 37 + (window.location.search !== destination.search && !preloadedResources.has(url)) 38 + ); 39 + } catch { 40 + return false; 41 + } 42 + } 43 + 44 + function hasLimitedBandwidth(): boolean { 45 + // Chrome thing 46 + // https://caniuse.com/?search=navigator.connection 47 + if ("connection" in navigator) { 48 + const networkInfo = (navigator as any).connection; 49 + return networkInfo.saveData || /2g/.test(networkInfo.effectiveType); 50 + } 51 + 52 + return false; 53 + }
crates/maudit/js/preload.ts

This is a binary file and will not be displayed.

+2
crates/maudit/src/assets.rs
··· 8 8 9 9 mod image; 10 10 pub mod image_cache; 11 + mod prefetch; 11 12 mod script; 12 13 mod style; 13 14 mod tailwind; 14 15 pub use image::{Image, ImageFormat, ImageOptions, ImagePlaceholder, RenderWithAlt, RenderedImage}; 16 + pub use prefetch::PrefetchPlugin; 15 17 pub use script::Script; 16 18 pub use style::{Style, StyleOptions}; 17 19 pub use tailwind::TailwindPlugin;
+49
crates/maudit/src/assets/prefetch.rs
··· 1 + use rolldown::{ 2 + ModuleType, 3 + plugin::{HookUsage, Plugin}, 4 + }; 5 + 6 + const PREFETCH_CODE: &str = include_str!("../../js/prefetch.ts"); 7 + 8 + /// Rolldown plugin to expose the prefetch module as a virtual module. 9 + #[derive(Debug)] 10 + pub struct PrefetchPlugin; 11 + 12 + impl Plugin for PrefetchPlugin { 13 + fn name(&self) -> std::borrow::Cow<'static, str> { 14 + "builtin:prefetch".into() 15 + } 16 + 17 + fn register_hook_usage(&self) -> HookUsage { 18 + HookUsage::ResolveId | HookUsage::Load 19 + } 20 + 21 + async fn resolve_id( 22 + &self, 23 + _ctx: &rolldown::plugin::PluginContext, 24 + args: &rolldown::plugin::HookResolveIdArgs<'_>, 25 + ) -> rolldown::plugin::HookResolveIdReturn { 26 + if args.specifier == "maudit:prefetch" { 27 + return Ok(Some(rolldown::plugin::HookResolveIdOutput { 28 + id: "maudit:prefetch".to_string().into(), 29 + ..Default::default() 30 + })); 31 + } 32 + Ok(None) 33 + } 34 + 35 + async fn load( 36 + &self, 37 + _ctx: &rolldown::plugin::PluginContext, 38 + args: &rolldown::plugin::HookLoadArgs<'_>, 39 + ) -> rolldown::plugin::HookLoadReturn { 40 + if args.id == "maudit:prefetch" { 41 + return Ok(Some(rolldown::plugin::HookLoadOutput { 42 + code: PREFETCH_CODE.to_string().into(), 43 + module_type: Some(ModuleType::Ts), 44 + ..Default::default() 45 + })); 46 + } 47 + Ok(None) 48 + } 49 + }
+54 -15
crates/maudit/src/build.rs
··· 10 10 11 11 use crate::{ 12 12 BuildOptions, BuildOutput, 13 - assets::{self, RouteAssets, TailwindPlugin, image_cache::ImageCache}, 13 + assets::{self, PrefetchPlugin, RouteAssets, Script, TailwindPlugin, image_cache::ImageCache}, 14 14 build::images::process_image, 15 15 content::ContentSources, 16 16 is_dev, ··· 130 130 .as_ref() 131 131 .map(|url| url.trim_end_matches('/')); 132 132 133 + let mut default_scripts = vec![]; 134 + 135 + if options.prefetch { 136 + let prefetch_script = Script::new( 137 + PathBuf::from("maudit:prefetch"), 138 + true, 139 + { 140 + use rapidhash::fast::RapidHasher; 141 + use std::hash::Hasher; 142 + 143 + let prefetch_content = include_str!("../js/prefetch.ts"); 144 + let mut buf = Vec::with_capacity(prefetch_content.len()); 145 + buf.extend_from_slice(include_str!("../js/prefetch.ts").as_bytes()); 146 + 147 + let mut hasher = RapidHasher::default(); 148 + hasher.write(&buf); 149 + 150 + let hash = hasher.finish(); 151 + let hex = format!("{:016x}", hash); 152 + hex[..5].to_string() 153 + }, 154 + &route_assets_options, 155 + ); 156 + default_scripts.push(prefetch_script); 157 + } 158 + 133 159 // This is fully serial. It is somewhat trivial to make it parallel, but it currently isn't because every time I've tried to 134 160 // (uncommited, #25, #41, #46) it either made no difference or was slower. The overhead of parallelism is just too high for 135 161 // how fast most sites build. Ideally, it'd be configurable and default to serial, but I haven't found an ergonomic way to do that yet. ··· 156 182 if base_params.is_empty() { 157 183 let mut route_assets = 158 184 RouteAssets::new(&route_assets_options, Some(image_cache.clone())); 185 + 186 + route_assets.scripts.extend(default_scripts.clone()); 159 187 160 188 let params = PageParams::default(); 161 189 let url = cached_route.url(&params); ··· 376 404 fs::create_dir_all(&route_assets_options.output_assets_dir)?; 377 405 } 378 406 379 - if !build_pages_styles.is_empty() || !build_pages_scripts.is_empty() { 407 + if true { 380 408 let assets_start = Instant::now(); 381 409 print_title("generating assets"); 382 410 ··· 409 437 .chain(css_inputs.into_iter()) 410 438 .collect::<Vec<InputItem>>(); 411 439 440 + println!( 441 + "Bundler inputs: {:?}", 442 + bundler_inputs 443 + .iter() 444 + .map(|input| input.import.clone()) 445 + .collect::<Vec<String>>() 446 + ); 447 + 412 448 if !bundler_inputs.is_empty() { 413 449 let mut module_types_hashmap = FxHashMap::default(); 414 450 module_types_hashmap.insert("woff".to_string(), ModuleType::Asset); ··· 427 463 module_types: Some(module_types_hashmap), 428 464 ..Default::default() 429 465 }, 430 - vec![Arc::new(TailwindPlugin { 431 - tailwind_path: options.assets.tailwind_binary_path.clone(), 432 - tailwind_entries: build_pages_styles 433 - .iter() 434 - .filter_map(|style| { 435 - if style.tailwind { 436 - Some(style.path().clone()) 437 - } else { 438 - None 439 - } 440 - }) 441 - .collect::<Vec<PathBuf>>(), 442 - })], 466 + vec![ 467 + Arc::new(TailwindPlugin { 468 + tailwind_path: options.assets.tailwind_binary_path.clone(), 469 + tailwind_entries: build_pages_styles 470 + .iter() 471 + .filter_map(|style| { 472 + if style.tailwind { 473 + Some(style.path().clone()) 474 + } else { 475 + None 476 + } 477 + }) 478 + .collect::<Vec<PathBuf>>(), 479 + }), 480 + Arc::new(PrefetchPlugin {}), 481 + ], 443 482 )?; 444 483 445 484 let _result = bundler.write().await?;
+3
crates/maudit/src/build/options.rs
··· 58 58 59 59 pub assets: AssetsOptions, 60 60 61 + pub prefetch: bool, 62 + 61 63 /// Options for sitemap generation. See [`SitemapOptions`] for configuration. 62 64 pub sitemap: SitemapOptions, 63 65 } ··· 149 151 output_dir: "dist".into(), 150 152 static_dir: "static".into(), 151 153 clean_output_dir: true, 154 + prefetch: true, 152 155 assets: AssetsOptions::default(), 153 156 sitemap: SitemapOptions::default(), 154 157 }
+1
xtask/Cargo.toml
··· 2 2 name = "xtask" 3 3 version = "0.1.0" 4 4 edition = "2024" 5 + publish = false 5 6 6 7 [dependencies] 7 8 rolldown = { package = "brk_rolldown", version = "0.2.3" }
+5 -2
xtask/src/main.rs
··· 62 62 input: Some(input_items), 63 63 dir: Some(js_dist_dir.to_string_lossy().to_string()), 64 64 format: Some(rolldown::OutputFormat::Esm), 65 + platform: Some(rolldown::Platform::Browser), 65 66 minify: Some(RawMinifyOptions::Bool(true)), 66 67 ..Default::default() 67 68 }; ··· 102 103 103 104 // Configure Rolldown bundler input 104 105 let input_items = vec![InputItem { 105 - name: Some("preload".to_string()), 106 - import: js_src_dir.join("preload.ts").to_string_lossy().to_string(), 106 + name: Some("prefetch".to_string()), 107 + import: js_src_dir.join("prefetch.ts").to_string_lossy().to_string(), 107 108 }]; 108 109 109 110 let bundler_options = BundlerOptions { 110 111 input: Some(input_items), 111 112 dir: Some(js_dist_dir.to_string_lossy().to_string()), 112 113 format: Some(rolldown::OutputFormat::Esm), 114 + platform: Some(rolldown::Platform::Browser), 115 + minify: Some(RawMinifyOptions::Bool(true)), 113 116 ..Default::default() 114 117 }; 115 118