simi-cli 0.1.8

A command line tool to help build, test, serve a Simi app
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use cargo_metadata;
use cargo_toml;
use toml;

use crate::build::BuildArgs;
use crate::error::*;
use crate::serve::ServeArgs;
use crate::test::TestArgs;

const STATIC_FOLDER: &'static str = "static";

fn user_static_index() -> String {
    format!("{}/index.html", STATIC_FOLDER)
}

fn user_static_style_scss() -> String {
    format!("{}/style.scss", STATIC_FOLDER)
}

pub enum SimiStage {
    UserSource,
    CargoBuild,
    WasmBindgen,
    SimiFinalApp,
}

#[derive(Deserialize)]
struct SimiToml {
    browser_drivers: Option<Vec<String>>,
    index: Option<String>,
    scss: Option<String>,
    output_path: Option<String>,
    wasm_serve_path: Option<String>,
}

#[derive(Debug)]
struct UserStatics {
    index: Option<PathBuf>,
    scss: Option<PathBuf>,
}

#[derive(Debug)]
pub struct SimiConfig {
    name: String,
    target: String,
    release: bool,
    nightly: bool,
    source_path: PathBuf,
    target_path: PathBuf,
    output_path: PathBuf,
    wasm_serve_path: Option<String>,
    serve_port: u16,
    browser_drivers: Vec<String>,
    with_head: bool,
    user_statics: UserStatics,
}

impl SimiToml {
    fn validate(&self) -> Result<(), Error> {
        if let Some(ref index) = self.index {
            let p = Path::new(&index);
            if let Err(e) = p.canonicalize() {
                return Err(format_err!("Invalid index file path={}: {}", index, e));
            }
        }
        if let Some(ref scss) = self.scss {
            let p = Path::new(&scss);
            if let Err(e) = p.canonicalize() {
                return Err(format_err!("Invalid scss file path={}: {}", scss, e));
            }
        }
        if let Some(ref output) = self.output_path {
            let p = Path::new(&output);
            if let Err(e) = p.canonicalize() {
                return Err(format_err!("Invalid output_path={}: {}", output, e));
            }
        }
        if let Some(ref drivers) = self.browser_drivers {
            for d in drivers {
                let p = Path::new(&d);
                if let Err(e) = p.canonicalize() {
                    return Err(format_err!("Invalid browser_drivers path={}: {}", d, e));
                }
            }
        }
        Ok(())
    }
}

impl UserStatics {
    fn new(simi_toml: &SimiToml, cwd: &PathBuf) -> Result<Self, Error> {
        let mut us = UserStatics {
            index: simi_toml.index.as_ref().map(PathBuf::from),
            scss: simi_toml.scss.as_ref().map(PathBuf::from),
        };
        if let Some(ref mut index) = us.index {
            let ok_index = index.canonicalize()?;
            *index = ok_index;
        } else {
            let index = cwd.clone().join(user_static_index());
            if index.exists() {
                us.index = Some(index);
            }
        }
        if let Some(ref mut scss) = us.scss {
            let ok_scss = scss.canonicalize()?;
            *scss = ok_scss;
        } else {
            let scss = cwd.clone().join(user_static_style_scss());
            if scss.exists() {
                us.scss = Some(scss);
            }
        }
        Ok(us)
    }
}

impl SimiConfig {
    fn new(arg: BuildArgs, serve_port: u16) -> Result<SimiConfig, Error> {
        let cwd = ::std::env::current_dir().expect("unable to get current working directory");
        let simi_toml = SimiConfig::load_simi_toml(&cwd)?;
        let user_statics = UserStatics::new(&simi_toml, &cwd)?;

        let cargo_toml = cwd.clone().join("Cargo.toml");
        let main_toml = cargo_toml::Manifest::from_slice(&fs::read(cargo_toml.as_path())?)?;

        // ?Unable to use: let metadata = cargo_metadata::metadata(Some(toml.as_path()))?;
        let metadata = match cargo_metadata::MetadataCommand::new()
            .manifest_path(cargo_toml.as_path())
            .exec()
        {
            Ok(value) => value,
            Err(e) => return Err(format_err!("error: {}", e)),
        };

        // TODO: Check if there is `simi` in the dependencies?
        // If not, it maybe not a simi app

        // default value for output_path
        let output_path = simi_toml
            .output_path
            .map_or(cwd.clone().join("simi-site"), PathBuf::from);

        let simi_config = SimiConfig {
            name: main_toml
                .package
                .expect("main package toml")
                .name
                .replace("-", "_"),
            target: crate::DEFAULT_TARGET.to_string(),
            release: arg.release,
            nightly: arg.nightly,
            source_path: cwd,
            target_path: PathBuf::from(&metadata.target_directory),
            output_path,
            wasm_serve_path: simi_toml
                .wasm_serve_path
                .map(|s| s.trim_end_matches('/').to_string()),
            serve_port,
            browser_drivers: simi_toml.browser_drivers.unwrap_or(Vec::new()),
            with_head: false,
            user_statics,
        };
        //println!("{:?}", simi_config);

        Ok(simi_config)
    }

    fn load_simi_toml(cwd: &PathBuf) -> Result<SimiToml, Error> {
        let simi_toml = cwd.join(".simi.toml");

        if simi_toml.exists() {
            let simi_toml: SimiToml = toml::from_slice(&fs::read(simi_toml.as_path())?)?;
            simi_toml.validate()?;
            Ok(simi_toml)
        } else {
            Ok(SimiToml {
                browser_drivers: None,
                index: None,
                scss: None,
                output_path: None,
                wasm_serve_path: None,
            })
        }
    }

    pub fn from_build(arg: BuildArgs) -> Result<SimiConfig, Error> {
        SimiConfig::new(arg, crate::DEFAULT_SERVE_PORT)
    }

    pub fn from_serve(arg: ServeArgs) -> Result<SimiConfig, Error> {
        let ServeArgs {
            build,
            port,
            serve_only: _,
        } = arg;
        SimiConfig::new(build, port)
    }

    pub fn for_test(arg: TestArgs) -> Result<SimiConfig, Error> {
        let mut config = SimiConfig::new(BuildArgs { release: false, nightly: arg.nightly }, crate::DEFAULT_SERVE_PORT)?;
        config.with_head = arg.with_head;
        Ok(config)
    }

    pub fn simi_app_name(&self) -> &String {
        &self.name
    }

    pub fn wasm_serve_path(&self) -> &Option<String> {
        &self.wasm_serve_path
    }

    pub fn target(&self) -> &String {
        &self.target
    }

    pub fn release(&self) -> bool {
        self.release
    }

    pub fn nightly(&self) -> bool {
        self.nightly
    }

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

    pub fn serve_port(&self) -> u16 {
        self.serve_port
    }

    pub fn get_wasm_file_name(&self, with_bg: bool) -> String {
        if with_bg {
            format!("{}_bg.wasm", self.name)
        } else {
            format!("{}.wasm", self.name)
        }
    }

    pub fn get_wasm_file_path(&self, stage: SimiStage) -> PathBuf {
        match stage {
            SimiStage::CargoBuild => {
                let mut rs = self.target_path.clone();
                rs.push(self.target());
                rs.push(if self.release { "release" } else { "debug" });
                rs.push(&self.get_wasm_file_name(false));
                rs
            }
            SimiStage::SimiFinalApp => {
                let mut rs = self.output_path.clone();
                rs.push(&self.get_wasm_file_name(true));
                rs
            }
            // wasm file will not be touched in other stages
            _ => unreachable!(),
        }
    }

    pub fn get_wasm_bindgen_js_file_path(&self, stage: SimiStage) -> PathBuf {
        match stage {
            SimiStage::WasmBindgen | SimiStage::SimiFinalApp => {
                self.output_path.join(format!("{}.js", self.name))
                //let mut rs = self.output_path.clone();
                //rs.push(&self.name);
                //rs.set_extension("js");
                //rs
            }
            // <simi_app_name>.js will not exist for other stage
            _ => unreachable!(),
        }
    }

    pub fn get_wasm_loader_js(&self) -> PathBuf {
        self.output_path.join(format!("{}_bg.js", self.name))
    }

    pub fn get_index_html_path(&self, stage: SimiStage) -> Option<PathBuf> {
        match stage {
            SimiStage::UserSource => self.user_statics.index.clone(),
            SimiStage::SimiFinalApp => {
                let mut rs = self.output_path.clone();
                rs.push("index.html");
                Some(rs)
            }
            _ => unreachable!(),
        }
    }

    pub fn get_scss_path(&self) -> &Option<PathBuf> {
        &self.user_statics.scss
    }

    pub fn get_css_path(&self) -> PathBuf {
        // This will always for the SimiStage::SimiFinalApp
        // Never be called if self.user_statics.scss is None
        self.output_path.join(self.get_css_file_name().unwrap())
    }

    pub fn get_css_file_name(&self) -> Option<String> {
        self.user_statics.scss.as_ref().map(|p| {
            p.file_name()
                .unwrap()
                .to_string_lossy()
                .replace(".scss", ".css")
        })
    }

    pub fn get_static_path(&self) -> PathBuf {
        self.source_path.join(STATIC_FOLDER)
    }

    pub fn has_browser_driver(&self) -> bool {
        self.browser_drivers.len() > 0
    }

    pub fn browser_drivers(&self) -> &Vec<String> {
        &self.browser_drivers
    }

    pub fn with_head(&self) -> bool {
        self.with_head
    }
}