diff --git a/cli/src/cargo.rs b/cli/src/cargo.rs index 30a56912..27307821 100644 --- a/cli/src/cargo.rs +++ b/cli/src/cargo.rs @@ -2,7 +2,7 @@ use std::process::{Command, Stdio}; use color_eyre::Result; -use crate::{config::Config, path}; +use crate::{config::Args, path}; macro_rules! feature { ($cargo_invocation: expr, $args: expr, { $($field: ident => $feature: literal),* $(,)? }) => { @@ -16,7 +16,7 @@ macro_rules! feature { }; } -pub fn invoke(cfg: &Config) -> Result<()> { +pub fn invoke(cfg: &Args) -> Result<()> { let mut cargo_invocation = Command::new("cargo"); cargo_invocation @@ -31,7 +31,7 @@ pub fn invoke(cfg: &Config) -> Result<()> { } else { Stdio::piped() }) - .env("TS_RS_EXPORT_DIR", path::absolute(&cfg.output_directory())?); + .env("TS_RS_EXPORT_DIR", path::absolute(cfg.output_directory())?); for (rust, ts) in &cfg.overrides { let env = format!("TS_RS_INTERNAL_OVERRIDE_{rust}"); diff --git a/cli/src/config.rs b/cli/src/config.rs index f7809545..40e32514 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -1,144 +1,153 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; - -use clap::Parser; -use color_eyre::{eyre::bail, owo_colors::OwoColorize, Result}; -use serde::Deserialize; - -#[derive(Parser, Debug)] -#[allow(clippy::struct_excessive_bools)] -pub struct Args { - /// Path to the `ts-rs` config file - #[arg(long)] - pub config: Option, - - /// Defines where your TS bindings will be saved by setting `TS_RS_EXPORT_DIR` - #[arg(long, short)] - pub output_directory: Option, - - /// Disables warnings caused by using serde attributes that ts-rs cannot process - #[arg(long)] - pub no_warnings: bool, - - /// Adds the ".js" extension to import paths - #[arg(long)] - pub esm_imports: bool, - - /// Formats the generated TypeScript files - #[arg(long)] - pub format: bool, - - /// Generates an index.ts file in your --output-directory that re-exports all - /// types generated by ts-rs - #[arg(long = "index")] - pub generate_index_ts: bool, - - /// Generates only a single index.ts file in your --output-directory that - /// contains all exported types - #[arg(long = "merge")] - pub merge_files: bool, - - /// Do not capture `cargo test`'s output, and pass --nocapture to the test binary - #[arg(long = "nocapture")] - pub no_capture: bool, -} - -// keeping this separate from `Args` for now :shrug: -#[derive(Default, Deserialize)] -#[serde(deny_unknown_fields, default, rename_all = "kebab-case")] -#[allow(clippy::struct_excessive_bools)] -pub struct Config { - // type overrides for types implemented inside ts-rs. - pub overrides: HashMap, - pub output_directory: Option, - pub no_warnings: bool, - pub esm_imports: bool, - pub format: bool, - - #[serde(rename = "index")] - pub generate_index_ts: bool, - - #[serde(rename = "merge")] - pub merge_files: bool, - - #[serde(rename = "nocapture")] - pub no_capture: bool, -} - -impl Config { - pub fn load() -> Result { - let args = Args::parse(); - - let cfg = Self::load_from_file(args.config.as_deref())?.merge(args); - cfg.verify()?; - Ok(cfg) - } - - pub fn output_directory(&self) -> &Path { - self.output_directory - .as_deref() - .expect("Output directory must not be `None`") - } - - fn load_from_file(path: Option<&Path>) -> Result { - if let Some(path) = path { - if !path.is_file() { - bail!("The provided path doesn't exist"); - } - - let content = std::fs::read_to_string(path)?; - return Ok(toml::from_str(&content)?); - } - - // TODO: from where do we actually load the config? - let path = Path::new("./ts-rs.toml"); - if !path.is_file() { - return Ok(Self::default()); - } - let content = std::fs::read_to_string(path)?; - Ok(toml::from_str(&content)?) - } - - fn verify(&self) -> Result<()> { - if self.merge_files && self.generate_index_ts { - bail!( - "{}: --index is not compatible with --merge", - "Error".bold().red() - ); - } - - if self.output_directory.is_none() { - bail!("{}: You must provide the output diretory, either through the config file or the --output-directory flag", "Error".bold().red()) - } - - Ok(()) - } - - fn merge( - mut self, - Args { - config: _, - output_directory, - no_warnings, - esm_imports, - format, - generate_index_ts, - merge_files, - no_capture, - }: Args, - ) -> Self { - // QUESTION: This gives the CLI flag priority over the config file's value, - // is this the correct order? - self.output_directory = output_directory.or(self.output_directory); - - self.no_warnings |= no_warnings; - self.esm_imports |= esm_imports; - self.format |= format; - self.generate_index_ts |= generate_index_ts; - self.merge_files |= merge_files; - self.no_capture |= no_capture; - self - } -} +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use clap::Parser; +use color_eyre::{eyre::bail, owo_colors::OwoColorize, Result}; +use serde::Deserialize; + +#[derive(Parser, Debug)] +#[allow(clippy::struct_excessive_bools)] +pub struct Args { + #[clap(skip)] + pub overrides: HashMap, + + /// Path to the `ts-rs` config file + #[arg(long)] + pub config: Option, + + /// Defines where your TS bindings will be saved by setting `TS_RS_EXPORT_DIR` + #[arg(long, short)] + pub output_directory: Option, + + /// Disables warnings caused by using serde attributes that ts-rs cannot process + #[arg(long)] + pub no_warnings: bool, + + /// Adds the ".js" extension to import paths + #[arg(long)] + pub esm_imports: bool, + + /// Formats the generated TypeScript files + #[arg(long)] + pub format: bool, + + /// Generates an index.ts file in your --output-directory that re-exports all + /// types generated by ts-rs + #[arg(long = "index")] + pub generate_index_ts: bool, + + /// Generates only a single index.ts file in your --output-directory that + /// contains all exported types + #[arg(long = "merge")] + pub merge_files: bool, + + /// Do not capture `cargo test`'s output, and pass --nocapture to the test binary + #[arg(long = "nocapture")] + pub no_capture: bool, +} + +// keeping this separate from `Args` for now :shrug: +#[derive(Default, Deserialize)] +#[serde(deny_unknown_fields, default, rename_all = "kebab-case")] +#[allow(clippy::struct_excessive_bools)] +pub struct Config { + /// Type overrides for types implemented inside ts-rs. + pub overrides: HashMap, + pub output_directory: Option, + pub no_warnings: bool, + pub esm_imports: bool, + pub format: bool, + + #[serde(rename = "index")] + pub generate_index_ts: bool, + + #[serde(rename = "merge")] + pub merge_files: bool, + + #[serde(rename = "nocapture")] + pub no_capture: bool, +} + +impl Args { + pub fn load() -> Result { + let mut args = Self::parse(); + + let cfg = Config::load_from_file(args.config.as_deref())?; + + args.merge(cfg); + args.verify()?; + + Ok(args) + } + + pub fn output_directory(&self) -> &Path { + self.output_directory + .as_deref() + .expect("Output directory must not be `None`") + } + + fn verify(&self) -> Result<()> { + if self.merge_files && self.generate_index_ts { + bail!( + "{}: --index is not compatible with --merge", + "Error".bold().red() + ); + } + + if self.output_directory.is_none() { + bail!("{}: You must provide the output diretory, either through the config file or the --output-directory flag", "Error".bold().red()) + } + + Ok(()) + } + + fn merge( + &mut self, + Config { + overrides, + output_directory, + no_warnings, + esm_imports, + format, + generate_index_ts, + merge_files, + no_capture, + }: Config, + ) { + // QUESTION: This gives the CLI flag priority over the config file's value, + // is this the correct order? + self.output_directory = output_directory.or_else(|| self.output_directory.clone()); + + self.overrides = overrides; + self.no_warnings |= no_warnings; + self.esm_imports |= esm_imports; + self.format |= format; + self.generate_index_ts |= generate_index_ts; + self.merge_files |= merge_files; + self.no_capture |= no_capture; + } +} + +impl Config { + fn load_from_file(path: Option<&Path>) -> Result { + if let Some(path) = path { + if !path.is_file() { + bail!("The provided path doesn't exist"); + } + + let content = std::fs::read_to_string(path)?; + return Ok(toml::from_str(&content)?); + } + + // TODO: from where do we actually load the config? + let path = Path::new("./ts-rs.toml"); + if !path.is_file() { + return Ok(Self::default()); + } + + let content = std::fs::read_to_string(path)?; + Ok(toml::from_str(&content)?) + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index f3e0c1fe..254637a1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -14,35 +14,33 @@ mod path; use metadata::{Metadata, FILE_NAME}; -use crate::config::Config; +use crate::config::Args; const BLANK_LINE: [u8; 2] = [b'\n', b'\n']; const NOTE: &[u8; 109] = b"// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n"; -struct Cleanup<'a>(&'a Config); -impl<'a> Drop for Cleanup<'a> { +impl Drop for Args { fn drop(&mut self) { - _ = fs::remove_file(self.0.output_directory().join(FILE_NAME)); + _ = fs::remove_file(self.output_directory().join(FILE_NAME)); } } fn main() -> Result<()> { color_eyre::install()?; - let cfg = Config::load()?; - let _cleanup = Cleanup(&cfg); + let args = Args::load()?; - let metadata_path = cfg.output_directory().join(FILE_NAME); + let metadata_path = args.output_directory().join(FILE_NAME); if metadata_path.exists() { fs::remove_file(&metadata_path)?; } - cargo::invoke(&cfg)?; + cargo::invoke(&args)?; let metadata_content = fs::read_to_string(&metadata_path)?; let metadata = Metadata::try_from(&*metadata_content)?; - let demand_unique_names = cfg.merge_files || cfg.generate_index_ts; + let demand_unique_names = args.merge_files || args.generate_index_ts; if !demand_unique_names || metadata.is_empty() { return Ok(()); @@ -59,7 +57,7 @@ fn main() -> Result<()> { return Ok(()); } - let index_path = cfg.output_directory().join("index.ts"); + let index_path = args.output_directory().join("index.ts"); if index_path.exists() { fs::remove_file(&index_path)?; @@ -72,7 +70,7 @@ fn main() -> Result<()> { index.write_all(NOTE)?; - if cfg.generate_index_ts { + if args.generate_index_ts { for path in metadata.export_paths() { index.write_fmt(format_args!("\nexport * from {path:?};"))?; } @@ -80,9 +78,9 @@ fn main() -> Result<()> { return Ok(()); } - if cfg.merge_files { + if args.merge_files { for path in metadata.export_paths() { - let path = path::absolute(cfg.output_directory().join(path))?; + let path = path::absolute(args.output_directory().join(path))?; let mut file = OpenOptions::new().read(true).open(&path)?; let mut buf = Vec::with_capacity(file.metadata()?.len().try_into()?); @@ -97,7 +95,7 @@ fn main() -> Result<()> { fs::remove_file(path)?; } - path::remove_empty_subdirectories(&cfg.output_directory())?; + path::remove_empty_subdirectories(args.output_directory())?; return Ok(()); }