use camino::Utf8PathBuf;
use std::{env, ffi::OsStr, fs, process::ExitCode};
#[derive(Debug, clap::Parser)]
#[clap(version)]
pub struct Args {
#[clap(subcommand)]
pub subcommand: Subcommand,
}
#[derive(Debug, clap::Subcommand)]
pub enum Subcommand {
Dev(SubcmdDev),
Build(SubcmdBuild),
Check(SubcmdCheck),
}
impl Args {
pub fn parse() -> Args {
<Args as clap::Parser>::parse()
}
}
pub fn main() -> ExitCode {
if is_global_installation() {
cd_to_project_root();
let mut args = env::args();
args.next();
let args: Vec<_> = args.map(|a| a.to_string()).collect();
let args_ref: Vec<_> = args.iter().map(|a| a.as_ref()).collect();
return result_to_code(execute("./node_modules/config-in-a-can/can", &args_ref));
}
let args = Args::parse();
result_to_code(match args.subcommand {
Subcommand::Dev(args) => subcmd_dev(args),
Subcommand::Build(args) => subcmd_build(args),
Subcommand::Check(args) => subcmd_check(args),
})
}
fn result_to_code(result: Result<(), ExitCode>) -> ExitCode {
match result {
Ok(_) => ExitCode::from(0),
Err(code) => code,
}
}
fn is_global_installation() -> bool {
let mut exe = env::current_exe().unwrap();
while exe.pop() {
if exe.file_name() == Some(OsStr::new("node_modules")) {
return false;
}
}
true
}
fn cd_to_project_root() {
let cwd = env::current_dir().unwrap();
let mut dir = Some(cwd.as_path());
while let Some(current) = dir {
let candidate = current.join("package.json");
if candidate.exists() {
env::set_current_dir(current).unwrap();
return;
}
dir = current.parent();
}
panic!("could not find package.json, unable to determine project root directory")
}
#[derive(Debug, clap::Args)]
pub struct SubcmdDev {
}
fn subcmd_dev(_args: SubcmdDev) -> Result<(), ExitCode> {
ensure_config();
execute_dependency_bin("vite", &[])
}
#[derive(Debug, clap::Args)]
pub struct SubcmdBuild {
}
fn subcmd_build(_args: SubcmdBuild) -> Result<(), ExitCode> {
ensure_config();
execute_dependency_bin("vite", &["build"])
}
#[derive(Debug, clap::Args)]
pub struct SubcmdCheck {
}
fn subcmd_check(_args: SubcmdCheck) -> Result<(), ExitCode> {
ensure_config();
let oxlint = execute_dependency_bin("oxlint", &[]);
let biome = execute_dependency_bin("biome", &["format"]);
let svelte_check = execute_dependency_bin("svelte-check", &["--tsconfig", "./tsconfig.json"]);
svelte_check.or(oxlint).or(biome)
}
fn execute_dependency_bin(bin: &str, args: &[&str]) -> Result<(), ExitCode> {
let mut path = Utf8PathBuf::from("./node_modules");
let can_path = fs::read_link("./node_modules/config-in-a-can").unwrap();
let can_path: Utf8PathBuf = can_path.try_into().unwrap();
path.push(can_path);
path.push("node_modules/.bin");
path.push(bin);
execute(path.as_str(), args)
}
fn execute(bin: &str, args: &[&str]) -> Result<(), ExitCode> {
use std::process::Command;
print!("$ {bin}");
for arg in args {
print!(" {arg}");
}
println!();
let mut fullpath = env::current_dir().unwrap();
fullpath.push(bin);
let status = Command::new(fullpath).args(args).status();
match status.map(|status| status.code()) {
Ok(Some(0)) => Ok(()),
Ok(Some(code)) => Err(ExitCode::from(code as u8)),
_ => Err(ExitCode::FAILURE),
}
}
#[derive(rust_embed::Embed)]
#[folder = "config/"]
struct Config;
fn ensure_config() {
println!("Generating config files:");
for path in Config::iter() {
let file = Config::get(&path).unwrap();
let path = path.strip_suffix(".template").unwrap_or(&path);
println!(" {path}");
fs::write(path, file.data).unwrap();
}
}