Rust library to generate static websites
5
fork

Configure Feed

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

refactor: separate tailwind plugin (#46)

* refactor: try some perf stuff

* fix: eh

* fix: oops

authored by

Erika and committed by
GitHub
51d40280 08c2a4d2

+189 -183
+1 -1
crates/maudit/Cargo.toml
··· 46 46 thiserror = "2.0.9" 47 47 oxc_sourcemap = "4.1.0" 48 48 rayon = "1.11.0" 49 - xxhash-rust = "0.8.15" 49 + xxhash-rust = "0.8.15"
+2
crates/maudit/src/assets.rs
··· 11 11 pub mod image_cache; 12 12 mod script; 13 13 mod style; 14 + mod tailwind; 14 15 pub use image::{Image, ImageFormat, ImageOptions}; 15 16 pub use script::Script; 16 17 pub use style::{Style, StyleOptions}; 18 + pub use tailwind::TailwindPlugin; 17 19 18 20 use crate::{AssetHashingStrategy, BuildOptions}; 19 21
+104
crates/maudit/src/assets/tailwind.rs
··· 1 + use std::{path::PathBuf, process::Command, time::Instant}; 2 + 3 + use log::info; 4 + use oxc_sourcemap::SourceMap; 5 + use rolldown::{ 6 + ModuleType, 7 + plugin::{HookUsage, Plugin}, 8 + }; 9 + 10 + /// Rolldown plugin to process select CSS files with the Tailwind CSS CLI. 11 + #[derive(Debug)] 12 + pub struct TailwindPlugin { 13 + pub tailwind_path: PathBuf, 14 + pub tailwind_entries: Vec<PathBuf>, 15 + } 16 + 17 + impl Plugin for TailwindPlugin { 18 + fn name(&self) -> std::borrow::Cow<'static, str> { 19 + "builtin:tailwind".into() 20 + } 21 + 22 + fn register_hook_usage(&self) -> rolldown::plugin::HookUsage { 23 + HookUsage::Transform 24 + } 25 + 26 + async fn transform( 27 + &self, 28 + _ctx: rolldown::plugin::SharedTransformPluginContext, 29 + args: &rolldown::plugin::HookTransformArgs<'_>, 30 + ) -> rolldown::plugin::HookTransformReturn { 31 + if *args.module_type != ModuleType::Css { 32 + return Ok(None); 33 + } 34 + 35 + if self 36 + .tailwind_entries 37 + .iter() 38 + .any(|entry| entry.canonicalize().unwrap().to_string_lossy() == args.id) 39 + { 40 + let start_tailwind = Instant::now(); 41 + let mut command = Command::new(&self.tailwind_path); 42 + command.args(["--input", args.id]); 43 + 44 + // Add minify in production, source maps in development 45 + if !crate::is_dev() { 46 + command.arg("--minify"); 47 + } 48 + if crate::is_dev() { 49 + command.arg("--map"); 50 + } 51 + 52 + let tailwind_output = command.output() 53 + .unwrap_or_else(|e| { 54 + // TODO: Return a proper error instead of panicking 55 + let args_str = if crate::is_dev() { 56 + format!("['--input', '{}', '--map']", args.id) 57 + } else { 58 + format!("['--input', '{}', '--minify']", args.id) 59 + }; 60 + panic!( 61 + "Failed to execute Tailwind CSS command, is it installed and is the path to its binary correct?\nCommand: '{}', Args: {}. Error: {}", 62 + &self.tailwind_path.display(), 63 + args_str, 64 + e 65 + ) 66 + }); 67 + 68 + if !tailwind_output.status.success() { 69 + let stderr = String::from_utf8_lossy(&tailwind_output.stderr); 70 + let error_message = format!( 71 + "Tailwind CSS process failed with status {}: {}", 72 + tailwind_output.status, stderr 73 + ); 74 + panic!("{}", error_message); 75 + } 76 + 77 + info!("Tailwind took {:?}", start_tailwind.elapsed()); 78 + 79 + let output = String::from_utf8_lossy(&tailwind_output.stdout); 80 + let (code, map) = if let Some((code, map)) = output.split_once("/*# sourceMappingURL") { 81 + (code.to_string(), Some(map.to_string())) 82 + } else { 83 + (output.to_string(), None) 84 + }; 85 + 86 + if let Some(map) = map { 87 + let source_map = SourceMap::from_json_string(&map).ok(); 88 + 89 + return Ok(Some(rolldown::plugin::HookTransformOutput { 90 + code: Some(code), 91 + map: source_map, 92 + ..Default::default() 93 + })); 94 + } 95 + 96 + return Ok(Some(rolldown::plugin::HookTransformOutput { 97 + code: Some(code), 98 + ..Default::default() 99 + })); 100 + } 101 + 102 + Ok(None) 103 + } 104 + }
+6 -180
crates/maudit/src/build.rs
··· 4 4 fs::{self}, 5 5 io::{self}, 6 6 path::{Path, PathBuf}, 7 - process::Command, 8 7 sync::Arc, 9 8 time::{Instant, SystemTime, UNIX_EPOCH}, 10 9 }; ··· 12 11 use crate::{ 13 12 BuildOptions, BuildOutput, 14 13 assets::{ 15 - self, RouteAssets, 14 + self, RouteAssets, TailwindPlugin, 16 15 image_cache::{IMAGE_CACHE_DIR, ImageCache}, 17 16 }, 18 17 build::images::process_image, 19 18 content::{ContentSources, RouteContent}, 20 - errors::BuildError, 21 19 is_dev, 22 20 logging::print_title, 23 - route::{DynamicRouteContext, FullRoute, PageContext, PageParams, RenderResult, RouteType}, 21 + route::{DynamicRouteContext, FullRoute, PageContext, PageParams, RouteType}, 24 22 }; 25 23 use colored::{ColoredString, Colorize}; 26 24 use log::{debug, info, trace, warn}; 27 - use oxc_sourcemap::SourceMap; 28 - use rolldown::{ 29 - Bundler, BundlerOptions, InputItem, ModuleType, 30 - plugin::{HookUsage, Plugin}, 31 - }; 25 + use rolldown::{Bundler, BundlerOptions, InputItem, ModuleType}; 32 26 use rustc_hash::{FxHashMap, FxHashSet}; 33 27 34 28 use crate::assets::Asset; 35 29 use crate::logging::{FormatElapsedTimeOptions, format_elapsed_time}; 36 - 37 - use lol_html::{RewriteStrSettings, element, rewrite_str}; 38 30 use rayon::prelude::*; 39 31 40 32 pub mod images; 41 33 pub mod metadata; 42 34 pub mod options; 43 35 44 - #[derive(Debug)] 45 - struct TailwindPlugin { 46 - tailwind_path: PathBuf, 47 - tailwind_entries: Vec<PathBuf>, 48 - } 49 - 50 - impl Plugin for TailwindPlugin { 51 - fn name(&self) -> std::borrow::Cow<'static, str> { 52 - "builtin:tailwind".into() 53 - } 54 - 55 - fn register_hook_usage(&self) -> rolldown::plugin::HookUsage { 56 - HookUsage::Transform 57 - } 58 - 59 - async fn transform( 60 - &self, 61 - _ctx: rolldown::plugin::SharedTransformPluginContext, 62 - args: &rolldown::plugin::HookTransformArgs<'_>, 63 - ) -> rolldown::plugin::HookTransformReturn { 64 - if *args.module_type != ModuleType::Css { 65 - return Ok(None); 66 - } 67 - 68 - if self 69 - .tailwind_entries 70 - .iter() 71 - .any(|entry| entry.canonicalize().unwrap().to_string_lossy() == args.id) 72 - { 73 - let start_tailwind = Instant::now(); 74 - let mut command = Command::new(&self.tailwind_path); 75 - command.args(["--input", args.id]); 76 - 77 - // Add minify in production, source maps in development 78 - if !crate::is_dev() { 79 - command.arg("--minify"); 80 - } 81 - if crate::is_dev() { 82 - command.arg("--map"); 83 - } 84 - 85 - let tailwind_output = command.output() 86 - .unwrap_or_else(|e| { 87 - // TODO: Return a proper error instead of panicking 88 - let args_str = if crate::is_dev() { 89 - format!("['--input', '{}', '--map']", args.id) 90 - } else { 91 - format!("['--input', '{}', '--minify']", args.id) 92 - }; 93 - panic!( 94 - "Failed to execute Tailwind CSS command, is it installed and is the path to its binary correct?\nCommand: '{}', Args: {}. Error: {}", 95 - &self.tailwind_path.display(), 96 - args_str, 97 - e 98 - ) 99 - }); 100 - 101 - if !tailwind_output.status.success() { 102 - let stderr = String::from_utf8_lossy(&tailwind_output.stderr); 103 - let error_message = format!( 104 - "Tailwind CSS process failed with status {}: {}", 105 - tailwind_output.status, stderr 106 - ); 107 - panic!("{}", error_message); 108 - } 109 - 110 - info!("Tailwind took {:?}", start_tailwind.elapsed()); 111 - 112 - let output = String::from_utf8_lossy(&tailwind_output.stdout); 113 - let (code, map) = if let Some((code, map)) = output.split_once("/*# sourceMappingURL") { 114 - (code.to_string(), Some(map.to_string())) 115 - } else { 116 - (output.to_string(), None) 117 - }; 118 - 119 - if let Some(map) = map { 120 - let source_map = SourceMap::from_json_string(&map).ok(); 121 - 122 - return Ok(Some(rolldown::plugin::HookTransformOutput { 123 - code: Some(code), 124 - map: source_map, 125 - ..Default::default() 126 - })); 127 - } 128 - 129 - return Ok(Some(rolldown::plugin::HookTransformOutput { 130 - code: Some(code), 131 - ..Default::default() 132 - })); 133 - } 134 - 135 - Ok(None) 136 - } 137 - } 138 - 139 36 pub fn execute_build( 140 37 routes: &[&dyn FullRoute], 141 38 content_sources: &mut ContentSources, ··· 220 117 221 118 let mut page_count = 0; 222 119 223 - // This is fully serial. It is trivial to make it parallel, but it currently isn't because every time I've tried to 224 - // (uncommited, #25 and #41) it either made no difference or was slower. The overhead of Rayon is just too high for 120 + // This is fully serial. It is somewhat trivial to make it parallel, but it currently isn't because every time I've tried to 121 + // (uncommited, #25 and #41) it either made no difference or was slower. The overhead of parallelism is just too high for 225 122 // 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. 123 + // If you manage to make it parallel and it actually improves performance, please open a PR! 226 124 for route in routes { 227 125 match route.route_type() { 228 126 RouteType::Static => { ··· 509 407 510 408 Ok(()) 511 409 } 512 - 513 - pub fn finish_route( 514 - render_result: RenderResult, 515 - page_assets: &assets::RouteAssets, 516 - route: String, 517 - ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { 518 - match render_result { 519 - // We've handled errors already at this point, but just in case, handle them again here 520 - RenderResult::Err(e) => Err(e), 521 - RenderResult::Text(html) => { 522 - let included_styles: Vec<_> = page_assets.included_styles().collect(); 523 - let included_scripts: Vec<_> = page_assets.included_scripts().collect(); 524 - 525 - if included_scripts.is_empty() && included_styles.is_empty() { 526 - return Ok(html.into_bytes()); 527 - } 528 - 529 - let element_content_handlers = vec![ 530 - // Add included scripts and styles to the head 531 - element!("head", |el| { 532 - for style in &included_styles { 533 - el.append( 534 - &format!( 535 - "<link rel=\"stylesheet\" href=\"{}\">", 536 - style.url().unwrap_or_else(|| panic!( 537 - "Failed to get URL for style: {:?}. This should not happen, please report this issue", 538 - style.path() 539 - )) 540 - ), 541 - lol_html::html_content::ContentType::Html, 542 - ); 543 - } 544 - 545 - for script in &included_scripts { 546 - el.append( 547 - &format!( 548 - "<script src=\"{}\" type=\"module\"></script>", 549 - script.url().unwrap_or_else(|| panic!( 550 - "Failed to get URL for script: {:?}. This should not happen, please report this issue.", 551 - script.path() 552 - )) 553 - ), 554 - lol_html::html_content::ContentType::Html, 555 - ); 556 - } 557 - 558 - Ok(()) 559 - }), 560 - ]; 561 - 562 - let output = rewrite_str( 563 - &html, 564 - RewriteStrSettings { 565 - element_content_handlers, 566 - ..RewriteStrSettings::new() 567 - }, 568 - )?; 569 - 570 - Ok(output.into_bytes()) 571 - } 572 - RenderResult::Raw(content) => { 573 - let included_styles: Vec<_> = page_assets.included_styles().collect(); 574 - let included_scripts: Vec<_> = page_assets.included_scripts().collect(); 575 - 576 - if !included_scripts.is_empty() || !included_styles.is_empty() { 577 - Err(BuildError::InvalidRenderResult { route })?; 578 - } 579 - 580 - Ok(content) 581 - } 582 - } 583 - }
+76 -2
crates/maudit/src/route.rs
··· 1 1 //! Core traits and structs to define the pages of your website. 2 2 //! 3 3 //! Every route must implement the [`Route`] trait. Then, pages can be passed to [`coronate()`](crate::coronate), through the [`routes!`](crate::routes) macro, to be built. 4 - use crate::assets::RouteAssets; 5 - use crate::build::finish_route; 4 + use crate::assets::{Asset, RouteAssets}; 6 5 use crate::content::{Entry, RouteContent}; 6 + use crate::errors::BuildError; 7 7 use crate::routing::{ 8 8 extract_params_from_raw_route, get_route_type_from_route_params, guess_if_route_is_endpoint, 9 9 }; 10 10 use rustc_hash::FxHashMap; 11 11 use std::any::Any; 12 12 use std::path::{Path, PathBuf}; 13 + 14 + use lol_html::{RewriteStrSettings, element, rewrite_str}; 13 15 14 16 /// The result of a page render, can be either text, raw bytes, or an error. 15 17 /// ··· 599 601 let bytes = finish_route(result, ctx.assets, self.route_raw())?; 600 602 601 603 Ok(bytes) 604 + } 605 + } 606 + 607 + pub fn finish_route( 608 + render_result: RenderResult, 609 + page_assets: &RouteAssets, 610 + route: String, 611 + ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { 612 + match render_result { 613 + // We've handled errors already at this point, but just in case, handle them again here 614 + RenderResult::Err(e) => Err(e), 615 + RenderResult::Text(html) => { 616 + let included_styles: Vec<_> = page_assets.included_styles().collect(); 617 + let included_scripts: Vec<_> = page_assets.included_scripts().collect(); 618 + 619 + if included_scripts.is_empty() && included_styles.is_empty() { 620 + return Ok(html.into_bytes()); 621 + } 622 + 623 + let element_content_handlers = vec![ 624 + // Add included scripts and styles to the head 625 + element!("head", |el| { 626 + for style in &included_styles { 627 + el.append( 628 + &format!( 629 + "<link rel=\"stylesheet\" href=\"{}\">", 630 + style.url().unwrap_or_else(|| panic!( 631 + "Failed to get URL for style: {:?}. This should not happen, please report this issue", 632 + style.path() 633 + )) 634 + ), 635 + lol_html::html_content::ContentType::Html, 636 + ); 637 + } 638 + 639 + for script in &included_scripts { 640 + el.append( 641 + &format!( 642 + "<script src=\"{}\" type=\"module\"></script>", 643 + script.url().unwrap_or_else(|| panic!( 644 + "Failed to get URL for script: {:?}. This should not happen, please report this issue.", 645 + script.path() 646 + )) 647 + ), 648 + lol_html::html_content::ContentType::Html, 649 + ); 650 + } 651 + 652 + Ok(()) 653 + }), 654 + ]; 655 + 656 + let output = rewrite_str( 657 + &html, 658 + RewriteStrSettings { 659 + element_content_handlers, 660 + ..RewriteStrSettings::new() 661 + }, 662 + )?; 663 + 664 + Ok(output.into_bytes()) 665 + } 666 + RenderResult::Raw(content) => { 667 + let included_styles: Vec<_> = page_assets.included_styles().collect(); 668 + let included_scripts: Vec<_> = page_assets.included_scripts().collect(); 669 + 670 + if !included_scripts.is_empty() || !included_styles.is_empty() { 671 + Err(BuildError::InvalidRenderResult { route })?; 672 + } 673 + 674 + Ok(content) 675 + } 602 676 } 603 677 } 604 678