diff --git a/Cargo.lock b/Cargo.lock index 90636dd0f..c76ab6321 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,6 +262,8 @@ version = "0.1.0" dependencies = [ "clap", "color-eyre", + "serde", + "toml", ] [[package]] @@ -1057,6 +1059,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1392,6 +1403,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tokio" version = "1.40.0" diff --git a/README.md b/README.md index 434edb970..01deb91f9 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,6 @@ Feel free to open an issue, discuss using GitHub discussions or open a PR. [See CONTRIBUTING.md](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md) ### MSRV -The Minimum Supported Rust Version for this crate is 1.63.0 +The Minimum Supported Rust Version for this crate is 1.72.0 License: MIT diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1236df2a7..94fb29e85 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -6,5 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { version = "4", features = ["derive"]} +clap = { version = "4", features = ["derive"] } color-eyre = "0.6" +serde = { version = "1", features = ["derive"] } +toml = "0.8" \ No newline at end of file diff --git a/cli/src/args.rs b/cli/src/args.rs deleted file mode 100644 index 86cde5f8f..000000000 --- a/cli/src/args.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::path::PathBuf; - -use clap::Parser; - -use crate::metadata::FILE_NAME; - -#[derive(Parser, Debug)] -#[allow(clippy::struct_excessive_bools)] -pub struct Args { - /// Defines where your TS bindings will be saved by setting TS_RS_EXPORT_DIR - #[arg(long, short)] - pub output_directory: PathBuf, - - /// 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, -} - -// Args is in scope for the entirety of the main function, so this will only -// be executed when the program is finished running. This helps prevent us -// from forgetting to do cleanup if some code branch early returns from main -impl Drop for Args { - fn drop(&mut self) { - _ = std::fs::remove_file(self.output_directory.join(FILE_NAME)); - } -} diff --git a/cli/src/cargo.rs b/cli/src/cargo.rs index a9695f247..27307821d 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::{args::Args, 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(args: &Args) -> Result<()> { +pub fn invoke(cfg: &Args) -> Result<()> { let mut cargo_invocation = Command::new("cargo"); cargo_invocation @@ -26,20 +26,25 @@ pub fn invoke(args: &Args) -> Result<()> { .arg("ts-rs/export") .arg("--features") .arg("ts-rs/generate-metadata") - .stdout(if args.no_capture { + .stdout(if cfg.no_capture { Stdio::inherit() } else { Stdio::piped() }) - .env("TS_RS_EXPORT_DIR", path::absolute(&args.output_directory)?); + .env("TS_RS_EXPORT_DIR", path::absolute(cfg.output_directory())?); - feature!(cargo_invocation, args, { + for (rust, ts) in &cfg.overrides { + let env = format!("TS_RS_INTERNAL_OVERRIDE_{rust}"); + cargo_invocation.env(env, ts); + } + + feature!(cargo_invocation, cfg, { no_warnings => "no-serde-warnings", esm_imports => "import-esm", format => "format", }); - if args.no_capture { + if cfg.no_capture { cargo_invocation.arg("--").arg("--nocapture"); } else { cargo_invocation.arg("--quiet"); diff --git a/cli/src/config.rs b/cli/src/config.rs new file mode 100644 index 000000000..40e32514a --- /dev/null +++ b/cli/src/config.rs @@ -0,0 +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 { + #[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 005684c57..254637a1f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -5,39 +5,36 @@ use std::{ io::{Read, Write}, }; -use clap::Parser; use color_eyre::{owo_colors::OwoColorize, Result}; -mod args; mod cargo; +mod config; mod metadata; mod path; -use args::Args; use metadata::{Metadata, FILE_NAME}; +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"; +impl Drop for Args { + fn drop(&mut self) { + _ = fs::remove_file(self.output_directory().join(FILE_NAME)); + } +} + fn main() -> Result<()> { color_eyre::install()?; - let args = Args::parse(); + let args = Args::load()?; - let metadata_path = args.output_directory.join(FILE_NAME); + let metadata_path = args.output_directory().join(FILE_NAME); if metadata_path.exists() { fs::remove_file(&metadata_path)?; } - if args.merge_files && args.generate_index_ts { - eprintln!( - "{} --index is not compatible with --merge", - "Error:".red().bold() - ); - - return Ok(()); - } - cargo::invoke(&args)?; let metadata_content = fs::read_to_string(&metadata_path)?; @@ -60,7 +57,7 @@ fn main() -> Result<()> { return Ok(()); } - let index_path = args.output_directory.join("index.ts"); + let index_path = args.output_directory().join("index.ts"); if index_path.exists() { fs::remove_file(&index_path)?; @@ -83,7 +80,7 @@ fn main() -> Result<()> { if args.merge_files { for path in metadata.export_paths() { - let path = path::absolute(args.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()?); @@ -98,7 +95,7 @@ fn main() -> Result<()> { fs::remove_file(path)?; } - path::remove_empty_subdirectories(&args.output_directory)?; + path::remove_empty_subdirectories(args.output_directory())?; return Ok(()); } diff --git a/cli/src/metadata.rs b/cli/src/metadata.rs index e04cc2bf7..7aee2f7a4 100644 --- a/cli/src/metadata.rs +++ b/cli/src/metadata.rs @@ -12,7 +12,7 @@ use color_eyre::{ pub const FILE_NAME: &str = "ts_rs.meta"; pub struct Metadata<'a> { - entries: std::collections::HashMap<&'a str, HashSet>>, + entries: HashMap<&'a str, HashSet>>, } impl<'a> TryFrom<&'a str> for Metadata<'a> { diff --git a/example/ts-rs.toml b/example/ts-rs.toml new file mode 100644 index 000000000..08506ebd2 --- /dev/null +++ b/example/ts-rs.toml @@ -0,0 +1,5 @@ +[overrides] +u64 = "number" +i64 = "number" +u128 = "number" +i128 = "number" \ No newline at end of file diff --git a/ts-rs/Cargo.toml b/ts-rs/Cargo.toml index e2fa9edf4..abbdceb25 100644 --- a/ts-rs/Cargo.toml +++ b/ts-rs/Cargo.toml @@ -15,7 +15,7 @@ categories = [ "web-programming", ] readme = "../README.md" -rust-version = "1.63.0" +rust-version = "1.72.0" [features] chrono-impl = ["chrono"] diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index c49a83cf2..2228d9dc2 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -116,7 +116,7 @@ //! [See CONTRIBUTING.md](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md) //! //! ## MSRV -//! The Minimum Supported Rust Version for this crate is 1.63.0 +//! The Minimum Supported Rust Version for this crate is 1.72.0 use std::{ any::TypeId, @@ -128,6 +128,7 @@ use std::{ }, ops::{Range, RangeInclusive}, path::{Path, PathBuf}, + sync::OnceLock, }; pub use ts_rs_macros::TS; @@ -613,12 +614,30 @@ impl Dependency { } } +static OVERRIDES: OnceLock> = OnceLock::new(); + +fn get_override(rust_type: &str) -> Option<&'static str> { + let overrides = OVERRIDES.get_or_init(|| { + const PREFIX: &str = "TS_RS_INTERNAL_OVERRIDE_"; + std::env::vars_os() + .flat_map(|(key, value)| Some((key.into_string().ok()?, value.into_string().ok()?))) + .filter(|(key, _)| key.starts_with(PREFIX)) + .map(|(key, value)| (&key.leak()[PREFIX.len()..], &*value.leak())) + .collect() + }); + overrides.get(rust_type).copied() +} + // generate impls for primitive types macro_rules! impl_primitives { ($($($ty:ty),* => $l:literal),*) => { $($( impl TS for $ty { type WithoutGenerics = Self; - fn name() -> String { $l.to_owned() } + fn name() -> String { + $crate::get_override(stringify!($ty)) + .unwrap_or($l) + .to_owned() + } fn inline() -> String { ::name() } fn inline_flattened() -> String { panic!("{} cannot be flattened", ::name()) } fn decl() -> String { panic!("{} cannot be declared", ::name()) } diff --git a/ts-rs/tests/integration/issue_308.rs b/ts-rs/tests/integration/issue_308.rs index be402b20f..41fc5e790 100644 --- a/ts-rs/tests/integration/issue_308.rs +++ b/ts-rs/tests/integration/issue_308.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use ts_rs::{TypeVisitor, Dependency, ExportError, TS}; +use ts_rs::{Dependency, ExportError, TypeVisitor, TS}; #[rustfmt::skip] trait Malicious {