mod copy_dir;
mod copy_file;
mod css;
mod html;
mod icon;
mod inline;
mod rust;
mod sass;
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{bail, ensure, Context, Result};
pub use html::HtmlPipeline;
use nipper::Document;
use serde::Deserialize;
use tokio::fs;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use crate::common::path_exists;
use crate::config::RtcBuild;
use crate::pipelines::copy_dir::{CopyDir, CopyDirOutput};
use crate::pipelines::copy_file::{CopyFile, CopyFileOutput};
use crate::pipelines::css::{Css, CssOutput};
use crate::pipelines::icon::{Icon, IconOutput};
use crate::pipelines::inline::{Inline, InlineOutput};
use crate::pipelines::rust::{RustApp, RustAppOutput};
use crate::pipelines::sass::{Sass, SassOutput};
const ATTR_INLINE: &str = "data-inline";
const ATTR_HREF: &str = "href";
const ATTR_TYPE: &str = "type";
const ATTR_REL: &str = "rel";
const SNIPPETS_DIR: &str = "snippets";
const TRUNK_ID: &str = "data-trunk-id";
pub type LinkAttrs = HashMap<String, String>;
#[allow(clippy::large_enum_variant)]
pub enum TrunkLink {
Css(Css),
Sass(Sass),
Icon(Icon),
Inline(Inline),
CopyFile(CopyFile),
CopyDir(CopyDir),
RustApp(RustApp),
}
impl TrunkLink {
pub async fn from_html(
cfg: Arc<RtcBuild>,
html_dir: Arc<PathBuf>,
ignore_chan: Option<mpsc::Sender<PathBuf>>,
attrs: LinkAttrs,
id: usize,
) -> Result<Self> {
let rel = attrs.get(ATTR_REL).context(
"all <link data-trunk .../> elements must have a `rel` attribute indicating the asset \
type",
)?;
Ok(match rel.as_str() {
Sass::TYPE_SASS | Sass::TYPE_SCSS => {
Self::Sass(Sass::new(cfg, html_dir, attrs, id).await?)
}
Icon::TYPE_ICON => Self::Icon(Icon::new(cfg, html_dir, attrs, id).await?),
Inline::TYPE_INLINE => Self::Inline(Inline::new(html_dir, attrs, id).await?),
Css::TYPE_CSS => Self::Css(Css::new(cfg, html_dir, attrs, id).await?),
CopyFile::TYPE_COPY_FILE => {
Self::CopyFile(CopyFile::new(cfg, html_dir, attrs, id).await?)
}
CopyDir::TYPE_COPY_DIR => Self::CopyDir(CopyDir::new(cfg, html_dir, attrs, id).await?),
RustApp::TYPE_RUST_APP => {
Self::RustApp(RustApp::new(cfg, html_dir, ignore_chan, attrs, id).await?)
}
_ => bail!(
r#"unknown <link data-trunk .../> attr value `rel="{}"`; please ensure the value is lowercase and is a supported asset type"#,
rel
),
})
}
pub fn spawn(self) -> JoinHandle<Result<TrunkLinkPipelineOutput>> {
match self {
TrunkLink::Css(inner) => inner.spawn(),
TrunkLink::Sass(inner) => inner.spawn(),
TrunkLink::Icon(inner) => inner.spawn(),
TrunkLink::Inline(inner) => inner.spawn(),
TrunkLink::CopyFile(inner) => inner.spawn(),
TrunkLink::CopyDir(inner) => inner.spawn(),
TrunkLink::RustApp(inner) => inner.spawn(),
}
}
}
pub enum TrunkLinkPipelineOutput {
Css(CssOutput),
Sass(SassOutput),
Icon(IconOutput),
Inline(InlineOutput),
CopyFile(CopyFileOutput),
CopyDir(CopyDirOutput),
RustApp(RustAppOutput),
}
impl TrunkLinkPipelineOutput {
pub async fn finalize(self, dom: &mut Document) -> Result<()> {
match self {
TrunkLinkPipelineOutput::Css(out) => out.finalize(dom).await,
TrunkLinkPipelineOutput::Sass(out) => out.finalize(dom).await,
TrunkLinkPipelineOutput::Icon(out) => out.finalize(dom).await,
TrunkLinkPipelineOutput::Inline(out) => out.finalize(dom).await,
TrunkLinkPipelineOutput::CopyFile(out) => out.finalize(dom).await,
TrunkLinkPipelineOutput::CopyDir(out) => out.finalize(dom).await,
TrunkLinkPipelineOutput::RustApp(out) => out.finalize(dom).await,
}
}
}
pub struct AssetFile {
pub path: PathBuf,
pub file_name: OsString,
pub file_stem: OsString,
pub ext: Option<String>,
}
impl AssetFile {
pub async fn new(rel_dir: &Path, mut path: PathBuf) -> Result<Self> {
if !path.is_absolute() {
path = rel_dir.join(path);
}
let path = fs::canonicalize(&path)
.await
.with_context(|| format!("error getting canonical path for {:?}", &path))?;
ensure!(
path_exists(&path).await?,
"target file does not appear to exist on disk {:?}",
&path
);
let file_name = match path.file_name() {
Some(file_name) => file_name.to_owned(),
None => bail!("asset has no file name {:?}", &path),
};
let file_stem = match path.file_stem() {
Some(file_stem) => file_stem.to_owned(),
None => bail!("asset has no file name stem {:?}", &path),
};
let ext = path
.extension()
.map(|ext| ext.to_owned().to_string_lossy().to_string());
Ok(Self {
path,
file_name,
file_stem,
ext,
})
}
pub async fn copy(&self, to_dir: &Path, with_hash: bool) -> Result<String> {
let bytes = fs::read(&self.path)
.await
.with_context(|| format!("error reading file for copying {:?}", &self.path))?;
let file_name = if with_hash {
format!(
"{}-{:x}.{}",
&self.file_stem.to_string_lossy(),
seahash::hash(bytes.as_ref()),
&self.ext.as_deref().unwrap_or_default()
)
} else {
self.file_name.to_string_lossy().into_owned()
};
let file_path = to_dir.join(&file_name);
fs::write(&file_path, bytes)
.await
.with_context(|| format!("error copying file {:?} to {:?}", &self.path, &file_path))?;
Ok(file_name)
}
pub async fn read_to_string(&self) -> Result<String> {
fs::read_to_string(&self.path)
.await
.with_context(|| format!("error reading file {:?} to string", self.path))
}
}
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PipelineStage {
PreBuild,
Build,
PostBuild,
}
pub(self) fn trunk_id_selector(id: usize) -> String {
format!(r#"link[{}="{}"]"#, TRUNK_ID, id)
}