use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{ensure, Context, Result};
use futures_util::stream::{FuturesUnordered, StreamExt};
use nipper::Document;
use tokio::fs;
use tokio::runtime::Handle;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use crate::config::RtcBuild;
use crate::hooks::{spawn_hooks, wait_hooks};
use crate::pipelines::rust::RustApp;
use crate::pipelines::{LinkAttrs, PipelineStage, TrunkLink, TrunkLinkPipelineOutput, TRUNK_ID};
const PUBLIC_URL_MARKER_ATTR: &str = "data-trunk-public-url";
const RELOAD_SCRIPT: &str = include_str!("../autoreload.js");
type AssetPipelineHandles = FuturesUnordered<JoinHandle<Result<TrunkLinkPipelineOutput>>>;
pub struct HtmlPipeline {
cfg: Arc<RtcBuild>,
target_html_path: PathBuf,
target_html_dir: Arc<PathBuf>,
ignore_chan: Option<mpsc::Sender<PathBuf>>,
}
impl HtmlPipeline {
pub fn new(cfg: Arc<RtcBuild>, ignore_chan: Option<mpsc::Sender<PathBuf>>) -> Result<Self> {
let target_html_path = cfg
.target
.canonicalize()
.context("failed to get canonical path of target HTML file")?;
let target_html_dir = Arc::new(
target_html_path
.parent()
.context("failed to determine parent dir of target HTML file")?
.to_owned(),
);
Ok(Self {
cfg,
target_html_path,
target_html_dir,
ignore_chan,
})
}
#[tracing::instrument(level = "trace", skip(self))]
pub fn spawn(self: Arc<Self>) -> JoinHandle<Result<()>> {
tokio::task::spawn_blocking(move || Handle::current().block_on(self.run()))
}
#[tracing::instrument(level = "trace", skip(self))]
async fn run(self: Arc<Self>) -> Result<()> {
tracing::info!("spawning asset pipelines");
wait_hooks(spawn_hooks(self.cfg.clone(), PipelineStage::PreBuild)).await?;
let raw_html = fs::read_to_string(&self.target_html_path).await?;
let mut target_html = Document::from(&raw_html);
let mut assets = vec![];
let links = target_html.select(r#"link[data-trunk]"#);
for (id, link) in links.nodes().iter().enumerate() {
link.set_attr(TRUNK_ID, &id.to_string());
let attrs = link
.attrs()
.into_iter()
.fold(LinkAttrs::new(), |mut acc, attr| {
acc.insert(attr.name.local.as_ref().to_string(), attr.value.to_string());
acc
});
let asset = TrunkLink::from_html(
self.cfg.clone(),
self.target_html_dir.clone(),
self.ignore_chan.clone(),
attrs,
id,
)
.await?;
assets.push(asset);
}
let rust_app_nodes = target_html
.select(r#"link[data-trunk][rel="rust"][data-type="main"], link[data-trunk][rel="rust"]:not([data-type])"#)
.length();
ensure!(
rust_app_nodes <= 1,
r#"only one <link data-trunk rel="rust" data-type="main" .../> may be specified"#
);
if rust_app_nodes == 0 {
let app = RustApp::new_default(
self.cfg.clone(),
self.target_html_dir.clone(),
self.ignore_chan.clone(),
)
.await?;
assets.push(TrunkLink::RustApp(app));
}
let mut pipelines: AssetPipelineHandles = FuturesUnordered::new();
pipelines.extend(assets.into_iter().map(|asset| asset.spawn()));
let build_hooks = spawn_hooks(self.cfg.clone(), PipelineStage::Build);
self.finalize_asset_pipelines(&mut target_html, pipelines)
.await?;
wait_hooks(build_hooks).await?;
self.finalize_html(&mut target_html);
let output_html = target_html.html().to_string(); fs::write(self.cfg.staging_dist.join("index.html"), &output_html)
.await
.context("error writing finalized HTML output")?;
wait_hooks(spawn_hooks(self.cfg.clone(), PipelineStage::PostBuild)).await?;
Ok(())
}
async fn finalize_asset_pipelines(
&self,
target_html: &mut Document,
mut pipelines: AssetPipelineHandles,
) -> Result<()> {
while let Some(asset_res) = pipelines.next().await {
let asset = asset_res
.context("failed to await asset finalization")?
.context("error from asset pipeline")?;
asset.finalize(target_html).await?;
}
Ok(())
}
fn finalize_html(&self, target_html: &mut Document) {
let mut base_elements =
target_html.select(&format!("html head base[{}]", PUBLIC_URL_MARKER_ATTR));
base_elements.remove_attr(PUBLIC_URL_MARKER_ATTR);
base_elements.set_attr("href", &self.cfg.public_url);
if self.cfg.inject_autoloader {
target_html
.select("body")
.append_html(format!("<script>{}</script>", RELOAD_SCRIPT));
}
}
}