use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use staticdatagen::compile;
use crate::cmd::SsgConfig;
use crate::{
accessibility, ai, assets, content, deploy, drafts, highlight, i18n,
livereload, pagination, plugin, plugins as plugins_mod, postprocess,
search, seo, shortcodes, taxonomy, walk,
};
#[derive(Debug, Clone)]
pub struct RunOptions {
pub quiet: bool,
pub include_drafts: bool,
pub deploy_target: Option<String>,
pub validate_only: bool,
pub jobs: Option<usize>,
}
impl RunOptions {
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
Self {
quiet: matches.get_flag("quiet"),
include_drafts: matches.get_flag("drafts"),
deploy_target: matches.get_one::<String>("deploy").cloned(),
validate_only: matches.get_flag("validate"),
jobs: matches.get_one::<usize>("jobs").copied(),
}
}
}
pub fn resolve_build_and_site_dirs(config: &SsgConfig) -> (PathBuf, PathBuf) {
let site_dir = config
.serve_dir
.clone()
.unwrap_or_else(|| config.output_dir.clone());
let build_dir = if site_dir == config.output_dir {
config.output_dir.with_extension("build-tmp")
} else {
config.output_dir.clone()
};
(build_dir, site_dir)
}
pub fn build_pipeline(
config: &SsgConfig,
opts: &RunOptions,
) -> (
plugin::PluginManager,
plugin::PluginContext,
PathBuf,
PathBuf,
) {
let (build_dir, site_dir) = resolve_build_and_site_dirs(config);
let ctx = plugin::PluginContext::with_config(
&config.content_dir,
&build_dir,
&site_dir,
&config.template_dir,
config.clone(),
);
let mut plugins = plugin::PluginManager::new();
register_default_plugins(
&mut plugins,
config,
opts.include_drafts,
opts.deploy_target.as_deref(),
);
(plugins, ctx, build_dir, site_dir)
}
pub fn execute_build_pipeline(
plugins: &plugin::PluginManager,
ctx: &plugin::PluginContext,
build_dir: &Path,
content_dir: &Path,
site_dir: &Path,
template_dir: &Path,
quiet: bool,
) -> Result<()> {
let start = std::time::Instant::now();
let cache = plugin::PluginCache::load(site_dir);
let mut ctx = ctx.clone();
ctx.cache = Some(cache);
plugins.run_before_compile(&ctx)?;
compile_site(build_dir, content_dir, site_dir, template_dir)?;
plugins.run_after_compile(&ctx)?;
if let Some(ref mut cache) = ctx.cache {
if let Ok(files) = walk::walk_files(site_dir, "html") {
for file in &files {
cache.update(file);
}
}
if let Err(e) = cache.save(site_dir) {
log::warn!("Failed to save plugin cache: {e}");
}
}
let elapsed = start.elapsed();
if !quiet {
println!(
"Site built in {:.2}s ({} plugin(s))",
elapsed.as_secs_f64(),
plugins.len()
);
}
Ok(())
}
pub fn compile_site(
build_dir: &Path,
content_dir: &Path,
site_dir: &Path,
template_dir: &Path,
) -> Result<()> {
compile(build_dir, content_dir, site_dir, template_dir).map_err(|e| {
eprintln!(" Error compiling site: {e:?}");
anyhow!("Failed to compile site: {e:?}")
})
}
pub fn register_default_plugins(
plugins: &mut plugin::PluginManager,
config: &SsgConfig,
include_drafts: bool,
deploy_target: Option<&str>,
) {
let base_url = config.base_url.clone();
plugins.register(content::ContentValidationPlugin);
plugins.register(drafts::DraftPlugin::new(include_drafts));
plugins.register(shortcodes::ShortcodePlugin);
#[cfg(feature = "tera-templates")]
plugins.register(crate::tera_plugin::TeraPlugin::from_template_dir(
&config.template_dir,
));
plugins.register(postprocess::SitemapFixPlugin);
plugins.register(postprocess::NewsSitemapFixPlugin);
plugins.register(postprocess::RssAggregatePlugin);
plugins.register(postprocess::AtomFeedPlugin);
plugins.register(postprocess::ManifestFixPlugin);
plugins.register(postprocess::HtmlFixPlugin);
plugins.register(highlight::HighlightPlugin::default());
plugins.register(seo::SeoPlugin);
plugins
.register(seo::JsonLdPlugin::from_site(&base_url, &config.site_name));
plugins.register(seo::CanonicalPlugin::new(base_url.clone()));
plugins.register(seo::RobotsPlugin::new(base_url));
plugins.register(ai::AiPlugin);
plugins.register(taxonomy::TaxonomyPlugin);
plugins.register(pagination::PaginationPlugin::default());
plugins.register(search::SearchPlugin);
plugins.register(accessibility::AccessibilityPlugin);
#[cfg(feature = "image-optimization")]
plugins.register(crate::image_plugin::ImageOptimizationPlugin::default());
if let Some(ref i18n_cfg) = config.i18n {
if i18n_cfg.locales.len() > 1 {
plugins.register(i18n::I18nPlugin::new(i18n_cfg.clone()));
}
}
plugins.register(assets::FingerprintPlugin);
plugins.register(plugins_mod::MinifyPlugin);
if let Some(target) = deploy_target {
let dt = match target {
"netlify" => Some(deploy::DeployTarget::Netlify),
"vercel" => Some(deploy::DeployTarget::Vercel),
"cloudflare" => Some(deploy::DeployTarget::CloudflarePages),
"github" => Some(deploy::DeployTarget::GithubPages),
_ => {
log::warn!("Unknown deploy target: {target}");
None
}
};
if let Some(dt) = dt {
plugins.register(deploy::DeployPlugin::new(dt));
}
}
plugins.register(livereload::LiveReloadPlugin::default());
}