Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/maudit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ dyn-eq = "0.1.3"
thiserror = "2.0.9"
oxc_sourcemap = "4.1.0"
rayon = "1.11.0"
xxhash-rust = "0.8.15"
xxhash-rust = "0.8.15"
2 changes: 2 additions & 0 deletions crates/maudit/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ mod image;
pub mod image_cache;
mod script;
mod style;
mod tailwind;
pub use image::{Image, ImageFormat, ImageOptions};
pub use script::Script;
pub use style::{Style, StyleOptions};
pub use tailwind::TailwindPlugin;

use crate::{AssetHashingStrategy, BuildOptions};

Expand Down
104 changes: 104 additions & 0 deletions crates/maudit/src/assets/tailwind.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::{path::PathBuf, process::Command, time::Instant};

use log::info;
use oxc_sourcemap::SourceMap;
use rolldown::{
ModuleType,
plugin::{HookUsage, Plugin},
};

/// Rolldown plugin to process select CSS files with the Tailwind CSS CLI.
#[derive(Debug)]
pub struct TailwindPlugin {
pub tailwind_path: PathBuf,
pub tailwind_entries: Vec<PathBuf>,
}

impl Plugin for TailwindPlugin {
fn name(&self) -> std::borrow::Cow<'static, str> {
"builtin:tailwind".into()
}

fn register_hook_usage(&self) -> rolldown::plugin::HookUsage {
HookUsage::Transform
}

async fn transform(
&self,
_ctx: rolldown::plugin::SharedTransformPluginContext,
args: &rolldown::plugin::HookTransformArgs<'_>,
) -> rolldown::plugin::HookTransformReturn {
if *args.module_type != ModuleType::Css {
return Ok(None);
}

if self
.tailwind_entries
.iter()
.any(|entry| entry.canonicalize().unwrap().to_string_lossy() == args.id)
{
let start_tailwind = Instant::now();
let mut command = Command::new(&self.tailwind_path);
command.args(["--input", args.id]);

// Add minify in production, source maps in development
if !crate::is_dev() {
command.arg("--minify");
}
if crate::is_dev() {
command.arg("--map");
}

let tailwind_output = command.output()
.unwrap_or_else(|e| {
// TODO: Return a proper error instead of panicking
let args_str = if crate::is_dev() {
format!("['--input', '{}', '--map']", args.id)
} else {
format!("['--input', '{}', '--minify']", args.id)
};
panic!(
"Failed to execute Tailwind CSS command, is it installed and is the path to its binary correct?\nCommand: '{}', Args: {}. Error: {}",
&self.tailwind_path.display(),
args_str,
e
)
});

if !tailwind_output.status.success() {
let stderr = String::from_utf8_lossy(&tailwind_output.stderr);
let error_message = format!(
"Tailwind CSS process failed with status {}: {}",
tailwind_output.status, stderr
);
panic!("{}", error_message);
}

info!("Tailwind took {:?}", start_tailwind.elapsed());

let output = String::from_utf8_lossy(&tailwind_output.stdout);
let (code, map) = if let Some((code, map)) = output.split_once("/*# sourceMappingURL") {
(code.to_string(), Some(map.to_string()))
} else {
(output.to_string(), None)
};

if let Some(map) = map {
let source_map = SourceMap::from_json_string(&map).ok();

return Ok(Some(rolldown::plugin::HookTransformOutput {
code: Some(code),
map: source_map,
..Default::default()
}));
}

return Ok(Some(rolldown::plugin::HookTransformOutput {
code: Some(code),
..Default::default()
}));
}

Ok(None)
}
}
186 changes: 6 additions & 180 deletions crates/maudit/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,138 +4,35 @@ use std::{
fs::{self},
io::{self},
path::{Path, PathBuf},
process::Command,
sync::Arc,
time::{Instant, SystemTime, UNIX_EPOCH},
};

use crate::{
BuildOptions, BuildOutput,
assets::{
self, RouteAssets,
self, RouteAssets, TailwindPlugin,
image_cache::{IMAGE_CACHE_DIR, ImageCache},
},
build::images::process_image,
content::{ContentSources, RouteContent},
errors::BuildError,
is_dev,
logging::print_title,
route::{DynamicRouteContext, FullRoute, PageContext, PageParams, RenderResult, RouteType},
route::{DynamicRouteContext, FullRoute, PageContext, PageParams, RouteType},
};
use colored::{ColoredString, Colorize};
use log::{debug, info, trace, warn};
use oxc_sourcemap::SourceMap;
use rolldown::{
Bundler, BundlerOptions, InputItem, ModuleType,
plugin::{HookUsage, Plugin},
};
use rolldown::{Bundler, BundlerOptions, InputItem, ModuleType};
use rustc_hash::{FxHashMap, FxHashSet};

use crate::assets::Asset;
use crate::logging::{FormatElapsedTimeOptions, format_elapsed_time};

use lol_html::{RewriteStrSettings, element, rewrite_str};
use rayon::prelude::*;

pub mod images;
pub mod metadata;
pub mod options;

#[derive(Debug)]
struct TailwindPlugin {
tailwind_path: PathBuf,
tailwind_entries: Vec<PathBuf>,
}

impl Plugin for TailwindPlugin {
fn name(&self) -> std::borrow::Cow<'static, str> {
"builtin:tailwind".into()
}

fn register_hook_usage(&self) -> rolldown::plugin::HookUsage {
HookUsage::Transform
}

async fn transform(
&self,
_ctx: rolldown::plugin::SharedTransformPluginContext,
args: &rolldown::plugin::HookTransformArgs<'_>,
) -> rolldown::plugin::HookTransformReturn {
if *args.module_type != ModuleType::Css {
return Ok(None);
}

if self
.tailwind_entries
.iter()
.any(|entry| entry.canonicalize().unwrap().to_string_lossy() == args.id)
{
let start_tailwind = Instant::now();
let mut command = Command::new(&self.tailwind_path);
command.args(["--input", args.id]);

// Add minify in production, source maps in development
if !crate::is_dev() {
command.arg("--minify");
}
if crate::is_dev() {
command.arg("--map");
}

let tailwind_output = command.output()
.unwrap_or_else(|e| {
// TODO: Return a proper error instead of panicking
let args_str = if crate::is_dev() {
format!("['--input', '{}', '--map']", args.id)
} else {
format!("['--input', '{}', '--minify']", args.id)
};
panic!(
"Failed to execute Tailwind CSS command, is it installed and is the path to its binary correct?\nCommand: '{}', Args: {}. Error: {}",
&self.tailwind_path.display(),
args_str,
e
)
});

if !tailwind_output.status.success() {
let stderr = String::from_utf8_lossy(&tailwind_output.stderr);
let error_message = format!(
"Tailwind CSS process failed with status {}: {}",
tailwind_output.status, stderr
);
panic!("{}", error_message);
}

info!("Tailwind took {:?}", start_tailwind.elapsed());

let output = String::from_utf8_lossy(&tailwind_output.stdout);
let (code, map) = if let Some((code, map)) = output.split_once("/*# sourceMappingURL") {
(code.to_string(), Some(map.to_string()))
} else {
(output.to_string(), None)
};

if let Some(map) = map {
let source_map = SourceMap::from_json_string(&map).ok();

return Ok(Some(rolldown::plugin::HookTransformOutput {
code: Some(code),
map: source_map,
..Default::default()
}));
}

return Ok(Some(rolldown::plugin::HookTransformOutput {
code: Some(code),
..Default::default()
}));
}

Ok(None)
}
}

pub fn execute_build(
routes: &[&dyn FullRoute],
content_sources: &mut ContentSources,
Expand Down Expand Up @@ -220,9 +117,10 @@ pub async fn build(

let mut page_count = 0;

// This is fully serial. It is trivial to make it parallel, but it currently isn't because every time I've tried to
// (uncommited, #25 and #41) it either made no difference or was slower. The overhead of Rayon is just too high for
// This is fully serial. It is somewhat trivial to make it parallel, but it currently isn't because every time I've tried to
// (uncommited, #25 and #41) it either made no difference or was slower. The overhead of parallelism is just too high for
// 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.
// If you manage to make it parallel and it actually improves performance, please open a PR!
for route in routes {
match route.route_type() {
RouteType::Static => {
Expand Down Expand Up @@ -509,75 +407,3 @@ fn write_route_file(content: &[u8], file_path: &PathBuf) -> Result<(), io::Error

Ok(())
}

pub fn finish_route(
render_result: RenderResult,
page_assets: &assets::RouteAssets,
route: String,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
match render_result {
// We've handled errors already at this point, but just in case, handle them again here
RenderResult::Err(e) => Err(e),
RenderResult::Text(html) => {
let included_styles: Vec<_> = page_assets.included_styles().collect();
let included_scripts: Vec<_> = page_assets.included_scripts().collect();

if included_scripts.is_empty() && included_styles.is_empty() {
return Ok(html.into_bytes());
}

let element_content_handlers = vec![
// Add included scripts and styles to the head
element!("head", |el| {
for style in &included_styles {
el.append(
&format!(
"<link rel=\"stylesheet\" href=\"{}\">",
style.url().unwrap_or_else(|| panic!(
"Failed to get URL for style: {:?}. This should not happen, please report this issue",
style.path()
))
),
lol_html::html_content::ContentType::Html,
);
}

for script in &included_scripts {
el.append(
&format!(
"<script src=\"{}\" type=\"module\"></script>",
script.url().unwrap_or_else(|| panic!(
"Failed to get URL for script: {:?}. This should not happen, please report this issue.",
script.path()
))
),
lol_html::html_content::ContentType::Html,
);
}

Ok(())
}),
];

let output = rewrite_str(
&html,
RewriteStrSettings {
element_content_handlers,
..RewriteStrSettings::new()
},
)?;

Ok(output.into_bytes())
}
RenderResult::Raw(content) => {
let included_styles: Vec<_> = page_assets.included_styles().collect();
let included_scripts: Vec<_> = page_assets.included_scripts().collect();

if !included_scripts.is_empty() || !included_styles.is_empty() {
Err(BuildError::InvalidRenderResult { route })?;
}

Ok(content)
}
}
}
Loading