use std::fs;
use std::io::Write as _;
use std::path::Path;
use std::process::Command;
use anyhow::{bail, Context, Result};
use rayon::prelude::*;
use crate::config::{BuildPlan, CssConfig, PageBuild};
pub fn ensure_node_modules(workspace_root: &Path) -> Result<()> {
if workspace_root.join("node_modules").exists() {
return Ok(());
}
eprintln!("[islands-build] node_modules missing; running `npm install`");
let status = Command::new("npm")
.arg("install")
.current_dir(workspace_root)
.status()
.context("spawn npm install")?;
if !status.success() {
bail!("npm install failed (exit {:?})", status.code());
}
Ok(())
}
fn run_postcss(workspace_root: &Path, input: &Path, output: &Path) -> Result<()> {
let input_arg = input.strip_prefix(workspace_root).unwrap_or(input);
let output_arg = output.strip_prefix(workspace_root).unwrap_or(output);
let result = Command::new("npx")
.arg("postcss")
.arg(input_arg)
.arg("-o")
.arg(output_arg)
.arg("--no-map")
.current_dir(workspace_root)
.output()
.with_context(|| format!("spawn npx postcss for {}", input.display()))?;
if !result.status.success() {
bail!(
"postcss failed (exit {:?}) for {}:\nstderr:\n{}\nstdout:\n{}",
result.status.code(),
input.display(),
String::from_utf8_lossy(&result.stderr),
String::from_utf8_lossy(&result.stdout)
);
}
Ok(())
}
#[cfg(feature = "basecoat")]
pub fn materialize_basecoat_css(target: &Path, enabled: bool) -> Result<()> {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
}
if enabled {
basecoat_css::write_to(target)
.with_context(|| format!("write basecoat-css source to {}", target.display()))?;
} else {
fs::write(target, "/* basecoat feature disabled */\n")
.with_context(|| format!("write empty basecoat.css stub to {}", target.display()))?;
}
Ok(())
}
fn build_base(workspace_root: &Path, out_dir: &Path, css: &CssConfig) -> Result<()> {
let output_dir = out_dir.join("css");
fs::create_dir_all(&output_dir).with_context(|| format!("create {}", output_dir.display()))?;
let output = output_dir.join("base.css");
eprintln!("[islands-build] building base.css → {}", output.display());
run_postcss(workspace_root, &css.base_css, &output)
}
fn generate_page_input(css: &CssConfig, page: &PageBuild) -> Result<()> {
fs::create_dir_all(&css.assets_css_dir)
.with_context(|| format!("create {}", css.assets_css_dir.display()))?;
let output_path = css.assets_css_dir.join(format!("page-{}.css", page.short_name()));
let mut content = String::new();
content.push_str(&css.theme_reference);
content.push_str("\n\n@import \"tailwindcss\";\n\n");
content.push_str(
"@layer base {\n *, ::before, ::after, ::backdrop { border-color: var(--color-border); }\n}\n\n",
);
for source in &page.css_sources {
content.push_str(&format!("@source \"{source}\";\n"));
}
let mut file =
fs::File::create(&output_path).with_context(|| format!("create {}", output_path.display()))?;
file.write_all(content.as_bytes())
.with_context(|| format!("write {}", output_path.display()))?;
Ok(())
}
fn build_page(workspace_root: &Path, out_dir: &Path, css: &CssConfig, page: &PageBuild) -> Result<()> {
generate_page_input(css, page)?;
let input = css.assets_css_dir.join(format!("page-{}.css", page.short_name()));
let output_dir = out_dir.join(&page.bundle_key);
fs::create_dir_all(&output_dir).with_context(|| format!("create {}", output_dir.display()))?;
let output = output_dir.join(format!("{}.css", page.underscore_name()));
eprintln!(
"[islands-build] building page CSS {} → {}",
page.bundle_key,
output.display()
);
run_postcss(workspace_root, &input, &output)
}
pub fn build_base_css(plan: &BuildPlan) -> Result<()> {
let Some(css) = &plan.css else {
return Ok(());
};
ensure_node_modules(&plan.workspace_root)?;
#[cfg(feature = "basecoat")]
materialize_basecoat_css(&css.assets_css_dir.join("basecoat.css"), css.basecoat)?;
#[cfg(not(feature = "basecoat"))]
if css.basecoat {
bail!(
"CssConfig.basecoat=true but islands-build was compiled without its `basecoat` feature"
);
}
build_base(&plan.workspace_root, &plan.out_dir, css)
}
pub fn build_page_css(plan: &BuildPlan, page: &PageBuild) -> Result<()> {
let Some(css) = &plan.css else {
return Ok(());
};
ensure_node_modules(&plan.workspace_root)?;
build_page(&plan.workspace_root, &plan.out_dir, css, page)
}
pub fn build_css(plan: &BuildPlan) -> Result<()> {
if plan.css.is_none() {
return Ok(());
}
build_base_css(plan)?;
plan.pages
.par_iter()
.try_for_each(|page| build_page_css(plan, page))?;
eprintln!("[islands-build] build-css complete");
Ok(())
}