ines-core 0.0.2

Rust bundler/framework for website with WebComponent
Documentation
mod builder;
pub mod config;
pub mod error;
mod include;

use error::Result;

use crate::project::config::Config;
use crate::project::error::Error::InvalidProject;
#[cfg(feature = "progress")]
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::path::PathBuf;
#[cfg(feature = "progress")]
use std::time::Duration;
use tracing::debug;
#[cfg(not(feature = "progress"))]
use tracing::info;

pub struct Project {
    path: PathBuf,
    config: Config,
}

impl Project {
    pub fn create(path: PathBuf) -> Result<Self> {
        debug!("creating project at {}", path.display());
        std::fs::create_dir_all(&path)?;
        let config = Config::default();
        config.write_to_file(&path.join("ines.toml"))?;
        std::fs::write(path.join("Cargo.toml"), include::CARGO_TOML)?;
        std::fs::write(path.join("build.rs"), include::BUILD_RS)?;
        std::fs::write(path.join("empty.rs"), "#![no_std]")?;
        std::fs::create_dir_all(path.join("src").join("components"))?;
        std::fs::create_dir_all(path.join("src").join("views"))?;
        std::fs::write(path.join("src").join("index.html"), include::INDEX_HTML)?;
        std::fs::write(
            path.join("src").join("index.js"),
            r#"import "./components/load.js""#,
        )?;
        std::fs::write(path.join("src").join("index.css"), "")?;
        std::fs::write(path.join("src").join("components").join("load.js"), "")?;
        std::fs::write(
            path.join("src").join("components").join("component.js"),
            include::COMPONENT_JS,
        )?;

        Ok(Self { path, config })
    }

    pub fn load_from_path(path: PathBuf) -> Result<Self> {
        debug!("loading project at {}", path.display());

        Self::is_file_or_dir(&path.join("ines.toml"))
            .take_if(|e| *e)
            .ok_or(InvalidProject("ines.toml not found".into()))?;
        Self::is_file_or_dir(&path.join("Cargo.toml"))
            .take_if(|e| *e)
            .ok_or(InvalidProject("Cargo.toml not found".into()))?;
        Self::is_file_or_dir(&path.join("build.rs"))
            .take_if(|e| *e)
            .ok_or(InvalidProject("build.rs not found".into()))?;
        Self::is_file_or_dir(&path.join("empty.rs"))
            .take_if(|e| *e)
            .ok_or(InvalidProject("empty.rs not found".into()))?;
        Self::is_file_or_dir(&path.join("src").join("index.html"))
            .take_if(|e| *e)
            .ok_or(InvalidProject("src/index.html not found".into()))?;
        Self::is_file_or_dir(&path.join("src").join("index.css"))
            .take_if(|e| *e)
            .ok_or(InvalidProject("src/index.css not found".into()))?;
        Self::is_file_or_dir(&path.join("src").join("index.js"))
            .take_if(|e| *e)
            .ok_or(InvalidProject("src/index.js not found".into()))?;
        Self::is_file_or_dir(&path.join("src").join("components").join("component.js"))
            .take_if(|e| *e)
            .ok_or(InvalidProject(
                "src/components/component.js not found".into(),
            ))?;
        Self::is_file_or_dir(&path.join("src").join("components").join("load.js"))
            .take_if(|e| *e)
            .ok_or(InvalidProject("src/components/load.js not found".into()))?;
        Self::is_file_or_dir(&path.join("src").join("views"))
            .take_if(|e| !*e)
            .ok_or(InvalidProject("src/views/ not found".into()))?;

        Self::_load_from_path(path)
    }

    fn is_file_or_dir(path: &PathBuf) -> Option<bool> {
        std::fs::metadata(path)
            .ok()
            .take_if(|e| e.is_dir() || e.is_file())
            .map(|e| e.is_file())
    }

    fn _load_from_path(path: PathBuf) -> Result<Self> {
        let config = Config::from_file(&path.join("ines.toml"))?;

        Ok(Self { path, config })
    }

    pub fn build(&self) -> Result<()> {
        #[cfg(feature = "progress")]
        let multi = MultiProgress::new();
        #[cfg(feature = "progress")]
        let run_style = ProgressStyle::with_template("{spinner:.cyan} {msg} ({elapsed})")
            .unwrap()
            .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
        #[cfg(feature = "progress")]
        let finish_style =
            ProgressStyle::with_template("{prefix:.green} {msg} (in {elapsed})").unwrap();

        #[cfg(feature = "progress")]
        let pb = {
            let pb = multi.add(
                ProgressBar::new_spinner()
                    .with_message("Copying base files")
                    .with_style(run_style.clone()),
            );
            pb.enable_steady_tick(Duration::from_millis(200));
            pb
        };

        let out_dir = self.path.join(&self.config.out);
        _ = std::fs::remove_dir_all(&out_dir);

        let strip = self.path.join("src");
        std::fs::create_dir_all(out_dir.join("components"))?;

        std::fs::copy(
            strip.join("components").join("load.js"),
            out_dir.join("components").join("load.js"),
        )?;
        std::fs::copy(
            strip.join("components").join("component.js"),
            out_dir.join("components").join("component.js"),
        )?;

        #[cfg(feature = "progress")]
        {
            pb.set_style(finish_style.clone());
            pb.set_message("Base files copied");
            pb.set_prefix("");
            pb.finish();
        }

        #[cfg(feature = "progress")]
        let pb = {
            let pb = multi.add(
                ProgressBar::new_spinner()
                    .with_message("Getting components")
                    .with_style(run_style.clone()),
            );
            pb.enable_steady_tick(Duration::from_millis(200));
            pb
        };

        #[cfg(not(feature = "progress"))]
        info!("getting components");
        let components = builder::scan_components(strip.join("components"))?;

        #[cfg(feature = "progress")]
        {
            pb.set_style(finish_style.clone());
            pb.set_message(format!("Got {} components", components.len()));
            pb.set_prefix("");
            pb.finish();
        }

        #[cfg(not(feature = "progress"))]
        info!("got {} components", components.len());

        #[cfg(feature = "progress")]
        let pb = {
            let pb = multi.add(
                ProgressBar::new_spinner()
                    .with_message("Building component")
                    .with_style(
                        ProgressStyle::with_template(
                            "{spinner:.cyan} {msg} {prefix:.grey} ({elapsed})",
                        )
                        .unwrap()
                        .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
                    ),
            );
            pb.enable_steady_tick(Duration::from_millis(200));
            pb
        };

        let mut htmls = vec![];
        for component in components {
            #[cfg(feature = "progress")]
            pb.set_prefix(component.name());

            #[cfg(not(feature = "progress"))]
            info!("building component {}", component.name());
            std::fs::create_dir_all(
                out_dir.join("components").join(
                    component
                        .get_base()
                        .strip_prefix(strip.join("components"))
                        .unwrap(),
                ),
            )?;
            std::fs::write(
                out_dir
                    .join("components")
                    .join(
                        component
                            .get_base()
                            .strip_prefix(strip.join("components"))
                            .unwrap(),
                    )
                    .join(component.filename()),
                component.build_js()?,
            )?;
            htmls.push(component.build_template()?);
        }

        #[cfg(feature = "progress")]
        {
            pb.set_prefix("");
            pb.finish();
        }

        #[cfg(feature = "progress")]
        let pb = {
            let pb = multi.add(
                ProgressBar::new_spinner()
                    .with_message("Generating index and assets")
                    .with_style(run_style.clone()),
            );
            pb.enable_steady_tick(Duration::from_millis(200));
            pb
        };

        #[cfg(not(feature = "progress"))]
        info!("generating index.html");
        std::fs::write(
            out_dir.join("index.html"),
            builder::build_html(self.path.join("src").join("index.html"), htmls.join("\n"))?,
        )?;

        #[cfg(not(feature = "progress"))]
        info!("copying assets");
        for entry in std::fs::read_dir(self.path.join("src"))?.flatten() {
            match entry.file_name().to_str() {
                Some("index.html") | Some("components") | Some("views") => {}
                _ => {
                    debug!(entry=?entry.path(), out=?out_dir.join(entry.file_name()));
                    builder::copy(entry.path(), out_dir.join(entry.file_name()))?;
                }
            }
        }

        #[cfg(feature = "progress")]
        {
            pb.set_style(finish_style.clone());
            pb.set_message("Generated index and assets");
            pb.set_prefix("");
            pb.finish();
        }

        #[cfg(feature = "progress")]
        let pb = {
            let pb = multi.add(
                ProgressBar::new_spinner()
                    .with_message("Generating redirect")
                    .with_style(run_style),
            );
            pb.enable_steady_tick(Duration::from_millis(200));
            pb
        };

        #[cfg(not(feature = "progress"))]
        info!("Generating redirects");
        if let Some(redirects) = &self.config.generate_redirects {
            for redirect in redirects {
                let path = redirect
                    .split("/")
                    .filter(|e| !e.is_empty())
                    .collect::<Vec<_>>()
                    .join("/");
                std::fs::create_dir_all(out_dir.join(&path))?;
                std::fs::write(
                    out_dir.join(&path).join("index.html"),
                    builder::generate_redirect(redirect),
                )?;
            }
        }

        #[cfg(feature = "progress")]
        {
            pb.set_style(finish_style);
            pb.set_message("Generated redirects");
            pb.set_prefix("");
            pb.finish();
        }

        Ok(())
    }

    pub fn base_path(&self) -> &PathBuf {
        &self.path
    }

    pub fn src_path(&self) -> PathBuf {
        self.base_path().join("src")
    }

    pub fn components_path(&self) -> PathBuf {
        self.src_path().join("components")
    }

    pub fn views_path(&self) -> PathBuf {
        self.src_path().join("views")
    }

    pub fn out_path(&self) -> PathBuf {
        self.base_path().join(&self.config.out)
    }
}