use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{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 indicatif::ProgressBar;
use nipper::Document;
use crate::config::RtcBuild;
use crate::pipelines::rust_app::RustApp;
use crate::pipelines::{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>,
progress: ProgressBar,
target_html_path: PathBuf,
target_html_dir: Arc<PathBuf>,
ignore_chan: Option<Sender<PathBuf>>,
}
impl HtmlPipeline {
pub fn new(cfg: Arc<RtcBuild>, progress: ProgressBar, 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()
.ok_or_else(|| anyhow!("failed to determine parent dir of target HTML file"))?
.to_owned(),
);
Ok(Self {
cfg,
progress,
target_html_path,
target_html_dir,
ignore_chan,
})
}
pub fn spawn(self: Arc<Self>) -> JoinHandle<Result<()>> {
spawn_local(self.build())
}
async fn build(self: Arc<Self>) -> Result<()> {
self.progress.set_message("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![];
for (id, mut link) in target_html.select(r#"link[data-trunk]"#).iter().enumerate() {
link.set_attr(TRUNK_ID, &id.to_string());
let asset = TrunkLink::from_html(
self.cfg.clone(),
self.progress.clone(),
self.target_html_dir.clone(),
self.ignore_chan.clone(),
link,
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" .../> link may be specified"#);
if rust_app_nodes == 0 {
let app = RustApp::new_default(
self.cfg.clone(),
self.progress.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(); fs::write(format!("{}/index.html", self.cfg.dist.display()), output_html.as_bytes())
.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?;
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);
}
}