diff --git a/Cargo.lock b/Cargo.lock index 09b0ae2..a5e0444 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,17 +405,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -[[package]] -name = "futures-macro" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "futures-sink" version = "0.3.30" @@ -435,11 +424,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", - "futures-macro", "futures-task", "pin-project-lite", "pin-utils", - "slab", ] [[package]] @@ -551,9 +538,7 @@ dependencies = [ "anyhow", "camino", "fs-err", - "futures-util", "hinoki_core", - "hyper", "hyper-util", "notify", "notify-debouncer-full", diff --git a/Cargo.toml b/Cargo.toml index d34e5bb..ce43192 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,9 +45,4 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } # Enable some optimizations for specific dependencies. # Otherwise, debug builds are unbearably slow. [profile.dev.package] -flate2 = { opt-level = 2 } -regex-automata = { opt-level = 2 } - -fancy-regex = { opt-level = 1 } -regex-syntax = { opt-level = 1 } syntect = { opt-level = 1 } diff --git a/components/core/src/build.rs b/components/core/src/build.rs index 1ba4d94..8b0253b 100644 --- a/components/core/src/build.rs +++ b/components/core/src/build.rs @@ -4,13 +4,15 @@ use anyhow::Context as _; use bumpalo_herd::Herd; use camino::Utf8Path; use fs_err::{self as fs}; +#[cfg(feature = "syntax-highlighting")] +use once_cell::sync::OnceCell; use rayon::iter::{ParallelBridge as _, ParallelIterator as _}; use tracing::{error, warn}; use walkdir::WalkDir; use crate::{ config::Config, - content::{ContentProcessor, ContentProcessorContext}, + content::{ContentProcessor, ContentProcessorContext, SyntaxHighlighter}, template::load_templates, }; @@ -18,70 +20,100 @@ mod output_dir; pub(crate) use self::output_dir::OutputDirManager; -pub fn build(config: &Config, include_drafts: bool) -> ExitCode { - fn build_inner( - config: &Config, - include_drafts: bool, - output_dir_mgr: &OutputDirManager, - ) -> anyhow::Result { - let alloc = Herd::new(); - let template_env = load_templates(&alloc)?; - let ctx = - ContentProcessorContext::new(config, include_drafts, template_env, output_dir_mgr); - rayon::scope(|scope| ContentProcessor::new(scope, &ctx).run())?; - Ok(ctx.did_error.load(Ordering::Relaxed)) +pub struct Build { + config: Config, + include_drafts: bool, + #[cfg(feature = "syntax-highlighting")] + syntax_highlighter: OnceCell, +} + +impl Build { + pub fn new(config: Config, include_drafts: bool) -> Self { + Self { + config, + include_drafts, + #[cfg(feature = "syntax-highlighting")] + syntax_highlighter: OnceCell::new(), + } } - fn copy_assets(output_dir_mgr: &OutputDirManager) -> anyhow::Result<()> { - WalkDir::new("theme/assets/").into_iter().par_bridge().try_for_each(|entry| { - let entry = entry.context("walking asset directory")?; - if entry.file_type().is_dir() { - return Ok(()); - } + pub fn config(&self) -> &Config { + &self.config + } - let Some(utf8_path) = Utf8Path::from_path(entry.path()) else { - warn!("Skipping non-utf8 file `{}`", entry.path().display()); - return Ok(()); - }; + pub fn run(&self) -> ExitCode { + fn copy_assets(output_dir_mgr: &OutputDirManager) -> anyhow::Result<()> { + WalkDir::new("theme/assets/").into_iter().par_bridge().try_for_each(|entry| { + let entry = entry.context("walking asset directory")?; + if entry.file_type().is_dir() { + return Ok(()); + } - let rel_path = - utf8_path.strip_prefix("theme/assets/").context("invalid WalkDir item")?; - let output_path = output_dir_mgr.output_path(rel_path, utf8_path)?; + let Some(utf8_path) = Utf8Path::from_path(entry.path()) else { + warn!("Skipping non-utf8 file `{}`", entry.path().display()); + return Ok(()); + }; - fs::copy(utf8_path, output_path).context("copying asset")?; - Ok(()) - }) - } + let rel_path = + utf8_path.strip_prefix("theme/assets/").context("invalid WalkDir item")?; + let output_path = output_dir_mgr.output_path(rel_path, utf8_path)?; - let output_dir_mgr = OutputDirManager::new(config.output_dir.clone()); + fs::copy(utf8_path, output_path).context("copying asset")?; + Ok(()) + }) + } - let (r1, r2) = rayon::join( - || build_inner(config, include_drafts, &output_dir_mgr), - || copy_assets(&output_dir_mgr), - ); + let output_dir_mgr = OutputDirManager::new(self.config.output_dir.clone()); - match (r1, r2) { - (Err(e1), Err(e2)) => { - error!("{e1:#}"); - error!("{e2:#}"); - ExitCode::FAILURE - } - (Ok(_), Err(e)) | (Err(e), Ok(_)) => { - error!("{e:#}"); - ExitCode::FAILURE + let (r1, r2) = + rayon::join(|| self.run_inner(&output_dir_mgr), || copy_assets(&output_dir_mgr)); + + match (r1, r2) { + (Err(e1), Err(e2)) => { + error!("{e1:#}"); + error!("{e2:#}"); + ExitCode::FAILURE + } + (Ok(_), Err(e)) | (Err(e), Ok(_)) => { + error!("{e:#}"); + ExitCode::FAILURE + } + (Ok(true), Ok(())) => ExitCode::FAILURE, + (Ok(false), Ok(())) => ExitCode::SUCCESS, } - (Ok(true), Ok(())) => ExitCode::FAILURE, - (Ok(false), Ok(())) => ExitCode::SUCCESS, } + + fn run_inner(&self, output_dir_mgr: &OutputDirManager) -> anyhow::Result { + let alloc = Herd::new(); + let template_env = load_templates(&alloc)?; + let ctx = ContentProcessorContext::new( + &self.config, + self.include_drafts, + template_env, + output_dir_mgr, + #[cfg(feature = "syntax-highlighting")] + &self.syntax_highlighter, + ); + rayon::scope(|scope| ContentProcessor::new(scope, &ctx).run())?; + Ok(ctx.did_error.load(Ordering::Relaxed)) + } +} + +pub fn build(config: Config, include_drafts: bool) -> ExitCode { + Build::new(config, include_drafts).run() } pub fn dump(config: Config) -> ExitCode { let output_dir_mgr = OutputDirManager::new("".into()); + #[cfg(feature = "syntax-highlighting")] + let syntax_highlighter = OnceCell::new(); let ctx = ContentProcessorContext::new( &config, true, minijinja::Environment::empty(), &output_dir_mgr, + #[cfg(feature = "syntax-highlighting")] + &syntax_highlighter, ); let res = rayon::scope(|scope| ContentProcessor::new(scope, &ctx).dump()); diff --git a/components/core/src/content.rs b/components/core/src/content.rs index 34a0444..75a6323 100644 --- a/components/core/src/content.rs +++ b/components/core/src/content.rs @@ -21,8 +21,6 @@ use tracing::{error, instrument, warn}; #[cfg(feature = "markdown")] use self::markdown::markdown_to_html; -#[cfg(feature = "syntax-highlighting")] -use self::syntax_highlighting::SyntaxHighlighter; use crate::{ build::OutputDirManager, config::Config, frontmatter::parse_frontmatter, metadata::metadata_env, template::functions, @@ -35,6 +33,8 @@ mod markdown; mod syntax_highlighting; pub(crate) use self::file_config::{ContentFileConfig, ProcessContent}; +#[cfg(feature = "syntax-highlighting")] +pub(crate) use self::syntax_highlighting::SyntaxHighlighter; pub(crate) struct ContentProcessor<'c, 's, 'sc> { // FIXME: args, template_env, syntax_highlighter (in ctx) plus render_scope @@ -278,7 +278,7 @@ pub(crate) struct ContentProcessorContext<'a> { include_drafts: bool, template_env: minijinja::Environment<'a>, #[cfg(feature = "syntax-highlighting")] - syntax_highlighter: OnceCell, + syntax_highlighter: &'a OnceCell, output_dir_mgr: &'a OutputDirManager, pub(crate) did_error: AtomicBool, } @@ -289,13 +289,14 @@ impl<'a> ContentProcessorContext<'a> { include_drafts: bool, template_env: minijinja::Environment<'a>, output_dir_mgr: &'a OutputDirManager, + #[cfg(feature = "syntax-highlighting")] syntax_highlighter: &'a OnceCell, ) -> Self { Self { config, include_drafts, template_env, #[cfg(feature = "syntax-highlighting")] - syntax_highlighter: OnceCell::new(), + syntax_highlighter, output_dir_mgr, did_error: AtomicBool::new(false), } diff --git a/components/core/src/content/syntax_highlighting.rs b/components/core/src/content/syntax_highlighting.rs index 55652cf..0dabe18 100644 --- a/components/core/src/content/syntax_highlighting.rs +++ b/components/core/src/content/syntax_highlighting.rs @@ -9,7 +9,7 @@ use syntect::{ }; use tracing::{error, warn}; -pub(super) struct SyntaxHighlighter { +pub(crate) struct SyntaxHighlighter { syntaxset: SyntaxSet, themes: BTreeMap, } diff --git a/components/dev_server/Cargo.toml b/components/dev_server/Cargo.toml index 484626f..f3dd7bb 100644 --- a/components/dev_server/Cargo.toml +++ b/components/dev_server/Cargo.toml @@ -6,10 +6,8 @@ edition = "2021" [dependencies] anyhow.workspace = true camino = "1.1.6" -futures-util = { version = "0.3.30", features = ["alloc", "std"] } fs-err.workspace = true hinoki_core = { path = "../core" } -hyper = { version = "1.0.0", features = ["http1", "http2", "server"] } hyper-util = { version = "0.1.2", features = ["http1", "http2", "tokio", "server-auto", "service"] } notify = "6.1.1" notify-debouncer-full = "0.3.1" diff --git a/components/dev_server/src/lib.rs b/components/dev_server/src/lib.rs index 33bafe6..9975df5 100644 --- a/components/dev_server/src/lib.rs +++ b/components/dev_server/src/lib.rs @@ -1,13 +1,14 @@ use std::{ + fmt, net::{Ipv6Addr, SocketAddr}, process::ExitCode, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; use camino::Utf8Path; use fs_err as fs; -use hinoki_core::{build::build, Config}; +use hinoki_core::{build::Build, Config}; use hyper_util::service::TowerToHyperService; use tempfile::tempdir; use tower_http::services::ServeDir; @@ -32,17 +33,23 @@ pub fn run(config: Config) -> ExitCode { async fn run_inner(mut config: Config) -> anyhow::Result<()> { let output_dir = tempdir()?; config.output_dir = output_dir.path().to_owned().try_into()?; - build(&config, true); - let _watch_guard = start_watch(&config)?; + let build = Build::new(config, true); + let begin = Instant::now(); + build.run(); + info!("Built site in {}", FormatDuration(begin.elapsed())); + + let config = build.config().clone(); + let _watch_guard = start_watch(build)?; serve(&config).await?; + Ok(()) } /// Start file notification watcher. /// /// Dropping the returned value stops the watcher thread. -fn start_watch(config: &Config) -> anyhow::Result { +fn start_watch(build: Build) -> anyhow::Result { use notify::{ event::{CreateKind, ModifyKind}, EventKind, RecursiveMode, Watcher, @@ -54,7 +61,6 @@ fn start_watch(config: &Config) -> anyhow::Result { let current_dir = fs::canonicalize(".")?; let mut debouncer = new_debouncer(DEBOUNCE_DURATION, None, { - let config = config.clone(); let current_dir = current_dir.clone(); move |res: DebounceEventResult| match res { Err(errors) => { @@ -84,7 +90,7 @@ fn start_watch(config: &Config) -> anyhow::Result { } }; - rel_path.starts_with(&config.path) + rel_path.starts_with(&build.config().path) || rel_path.starts_with("content") || rel_path.starts_with("theme") }); @@ -93,7 +99,9 @@ fn start_watch(config: &Config) -> anyhow::Result { }); if !events.is_empty() { - build(&config, true); + let begin = Instant::now(); + build.run(); + info!("Rebuilt site in {}", FormatDuration(begin.elapsed())); } } } @@ -129,3 +137,27 @@ async fn serve(config: &Config) -> anyhow::Result<()> { }); } } + +struct FormatDuration(Duration); + +impl fmt::Display for FormatDuration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let duration = self.0; + let total_secs = duration.as_secs(); + if let hours @ 1.. = total_secs / 3600 { + let minutes = total_secs / 60 % 60; + return write!(f, "{hours}h {minutes}min"); + } + if let minutes @ 1.. = total_secs / 60 { + let secs = total_secs % 60; + return write!(f, "{minutes}min {secs}s"); + } + + let millis = duration.as_millis(); + if total_secs > 0 { + write!(f, "{total_secs}s ")?; + } + + write!(f, "{millis}ms") + } +} diff --git a/src/main.rs b/src/main.rs index eed35c3..77703bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,7 +31,7 @@ fn main() -> ExitCode { }; match args.command { - Command::Build(args) => build(&config, args.include_drafts), + Command::Build(args) => build(config, args.include_drafts), Command::DumpMetadata => dump(config), #[cfg(feature = "dev-server")] Command::Serve => hinoki_dev_server::run(config),