use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{ensure, Context, Result};
use async_std::fs;
use async_std::task::{spawn_local, JoinHandle};
use futures::channel::mpsc::Sender;
use futures::stream::{FuturesUnordered, StreamExt};
use nipper::Document;
use crate::config::RtcBuild;
use crate::pipelines::rust_app::RustApp;
use crate::pipelines::{LinkAttrs, TrunkLink, TrunkLinkPipelineOutput, TRUNK_ID};
const PUBLIC_URL_MARKER_ATTR: &str = "data-trunk-public-url";
type AssetPipelineHandles = FuturesUnordered<JoinHandle<Result<TrunkLinkPipelineOutput>>>;
pub struct HtmlPipeline {
cfg: Arc<RtcBuild>,
target_html_path: PathBuf,
target_html_dir: Arc<PathBuf>,
ignore_chan: Option<Sender<PathBuf>>,
}
impl HtmlPipeline {
pub fn new(cfg: Arc<RtcBuild>, ignore_chan: Option<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<()>> {
spawn_local(self.run())
}
#[tracing::instrument(level = "trace", skip(self))]
async fn run(self: Arc<Self>) -> Result<()> {
tracing::info!("spawning asset pipelines");
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"]"#).length();
ensure!(rust_app_nodes <= 1, r#"only one <link data-trunk rel="rust" .../> 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()));
self.finalize_asset_pipelines(&mut target_html, pipelines).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")?;
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 spawn assets finalization")?;
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);
}
}