diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab65bf9a..636b3b343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,10 @@ - Added `TS::dependency_types()` ([#221](https://github.com/Aleph-Alpha/ts-rs/pull/221)) - Added `TS::generics()` ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241)) - Added `TS::WithoutGenerics` ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241)) -- `Result`, `Option`, `HashMap` and `Vec` had their implementations of `TS` changed ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241)) - Removed `TS::transparent()` ([#243](https://github.com/Aleph-Alpha/ts-rs/pull/243)) +- Handling of output paths ([#247](https://github.com/Aleph-Alpha/ts-rs/pull/247), [#250](https://github.com/Aleph-Alpha/ts-rs/pull/250), [#256](https://github.com/Aleph-Alpha/ts-rs/pull/256)) + - All paths specified using `#[ts(export_to = "...")]` are now relative to `TS_RS_EXPORT_DIR`, which defaults to `./bindings/` +- Replace `TS::export` with `TS::export`, `TS::export_all` and `TS::export_to_all` ([#263](https://github.com/Aleph-Alpha/ts-rs/pull/263)) ### Features @@ -27,11 +29,14 @@ - Support `#[serde(untagged)]` on individual enum variants ([#226](https://github.com/Aleph-Alpha/ts-rs/pull/226)) - Support for `#[serde(rename_all_fields = "...")]` ([#225](https://github.com/Aleph-Alpha/ts-rs/pull/225)) - Export Rust doc comments/attributes on structs/enums as TSDoc strings ([#187](https://github.com/Aleph-Alpha/ts-rs/pull/187)) +- `Result`, `Option`, `HashMap` and `Vec` had their implementations of `TS` changed ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241)) - Implement `#[ts(...)]` equivalent for `#[serde(tag = "...")]` being used on a struct with named fields ([#244](https://github.com/Aleph-Alpha/ts-rs/pull/244)) ### Fixes -- fix `#[ts(skip)]` and `#[serde(skip)]` in variants of adjacently or internally tagged enums ([#231](https://github.com/Aleph-Alpha/ts-rs/pull/231)) +- Fix `#[ts(skip)]` and `#[serde(skip)]` in variants of adjacently or internally tagged enums ([#231](https://github.com/Aleph-Alpha/ts-rs/pull/231)) - `rename_all` with `camelCase` produces wrong names if fields were already in camelCase ([#198](https://github.com/Aleph-Alpha/ts-rs/pull/198)) - Improve support for references ([#199](https://github.com/Aleph-Alpha/ts-rs/pull/199)) - Generic type aliases generate correctly ([#233](https://github.com/Aleph-Alpha/ts-rs/pull/233)) +- Improve compiler errors ([#257](https://github.com/Aleph-Alpha/ts-rs/pull/257)) +- Update dependencies ([#255](https://github.com/Aleph-Alpha/ts-rs/pull/255)) \ No newline at end of file diff --git a/macros/src/lib.rs b/macros/src/lib.rs index f66aa3ef9..558d7dcec 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -44,11 +44,8 @@ impl DerivedTS { }; quote! { - fn output_path() -> Option { - let path = std::env::var("TS_RS_EXPORT_DIR"); - let path = path.as_deref().unwrap_or("./bindings"); - - Some(std::path::Path::new(path).join(#path)) + fn output_path() -> Option<&'static std::path::Path> { + Some(std::path::Path::new(#path)) } } }; @@ -164,7 +161,7 @@ impl DerivedTS { #[cfg(test)] #[test] fn #test_fn() { - #ty::export().expect("could not export type"); + #ty::export_all().expect("could not export type"); } } } diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 12fe35505..07d35dfcc 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -1,12 +1,14 @@ use std::{ any::TypeId, + borrow::Cow, collections::BTreeMap, fmt::Write, + fs::File, path::{Component, Path, PathBuf}, sync::Mutex, }; -pub(crate) use recursive_export::export_type_with_dependencies; +pub(crate) use recursive_export::export_all_into; use thiserror::Error; use crate::TS; @@ -30,16 +32,27 @@ pub enum ExportError { } mod recursive_export { - use std::{any::TypeId, collections::HashSet}; + use std::{any::TypeId, collections::HashSet, path::Path}; - use super::export_type; + use super::export_into; use crate::{ typelist::{TypeList, TypeVisitor}, ExportError, TS, }; + /// Exports `T` to the file specified by the `#[ts(export_to = ..)]` attribute within the given + /// base directory. + /// Additionally, all dependencies of `T` will be exported as well. + pub(crate) fn export_all_into( + out_dir: impl AsRef, + ) -> Result<(), ExportError> { + let mut seen = HashSet::new(); + export_recursive::(&mut seen, out_dir) + } + struct Visit<'a> { seen: &'a mut HashSet, + out_dir: &'a Path, error: Option, } @@ -51,29 +64,27 @@ mod recursive_export { return; } - self.error = export_recursive::(self.seen).err(); + self.error = export_recursive::(self.seen, self.out_dir).err(); } } - /// Exports `T` to the file specified by the `#[ts(export_to = ..)]` attribute. - /// Additionally, all dependencies of `T` will be exported as well. - pub(crate) fn export_type_with_dependencies( - ) -> Result<(), ExportError> { - let mut seen = HashSet::new(); - export_recursive::(&mut seen) - } - // exports T, then recursively calls itself with all of its dependencies fn export_recursive( seen: &mut HashSet, + out_dir: impl AsRef, ) -> Result<(), ExportError> { if !seen.insert(TypeId::of::()) { return Ok(()); } + let out_dir = out_dir.as_ref(); - export_type::()?; + export_into::(out_dir)?; - let mut visitor = Visit { seen, error: None }; + let mut visitor = Visit { + seen, + out_dir, + error: None, + }; T::dependency_types().for_each(&mut visitor); if let Some(e) = visitor.error { @@ -85,15 +96,19 @@ mod recursive_export { } /// Export `T` to the file specified by the `#[ts(export_to = ..)]` attribute -pub(crate) fn export_type() -> Result<(), ExportError> { +pub(crate) fn export_into( + out_dir: impl AsRef, +) -> Result<(), ExportError> { let path = T::output_path() .ok_or_else(std::any::type_name::) .map_err(ExportError::CannotBeExported)?; - export_type_to::(path::absolute(path)?) + let path = out_dir.as_ref().join(path); + + export_to::(path::absolute(path)?) } /// Export `T` to the file specified by the `path` argument. -pub(crate) fn export_type_to>( +pub(crate) fn export_to>( path: P, ) -> Result<(), ExportError> { // Lock to make sure only one file will be written at a time. @@ -102,7 +117,7 @@ pub(crate) fn export_type_to>( static FILE_LOCK: Mutex<()> = Mutex::new(()); #[allow(unused_mut)] - let mut buffer = export_type_to_string::()?; + let mut buffer = export_to_string::()?; // format output #[cfg(feature = "format")] @@ -121,20 +136,35 @@ pub(crate) fn export_type_to>( std::fs::create_dir_all(parent)?; } let lock = FILE_LOCK.lock().unwrap(); - std::fs::write(path.as_ref(), buffer)?; + { + // Manually write to file & call `sync_data`. Otherwise, calling `fs::read(path)` + // immediately after `T::export()` might result in an empty file. + use std::io::Write; + let mut file = File::create(path)?; + file.write_all(buffer.as_bytes())?; + file.sync_data()?; + } + drop(lock); Ok(()) } -/// Returns the generated defintion for `T`. -pub(crate) fn export_type_to_string() -> Result { +/// Returns the generated definition for `T`. +pub(crate) fn export_to_string() -> Result { let mut buffer = String::with_capacity(1024); buffer.push_str(NOTE); - generate_imports::(&mut buffer)?; + generate_imports::(&mut buffer, default_out_dir())?; generate_decl::(&mut buffer); Ok(buffer) } +pub(crate) fn default_out_dir() -> Cow<'static, Path> { + match std::env::var("TS_RS_EXPORT_DIR") { + Err(..) => Cow::Borrowed(Path::new("./bindings")), + Ok(dir) => Cow::Owned(PathBuf::from(dir)), + } +} + /// Push the declaration of `T` fn generate_decl(out: &mut String) { // Type Docs @@ -148,11 +178,15 @@ fn generate_decl(out: &mut String) { out.push_str(&T::decl()); } -/// Push an import statement for all dependencies of `T` -fn generate_imports(out: &mut String) -> Result<(), ExportError> { +/// Push an import statement for all dependencies of `T`. +fn generate_imports( + out: &mut String, + out_dir: impl AsRef, +) -> Result<(), ExportError> { let path = T::output_path() .ok_or_else(std::any::type_name::) .map_err(ExportError::CannotBeExported)?; + let path = out_dir.as_ref().join(path); let deps = T::dependencies(); let deduplicated_deps = deps @@ -162,7 +196,8 @@ fn generate_imports(out: &mut String) -> Result<(), Ex .collect::>(); for (_, dep) in deduplicated_deps { - let rel_path = import_path(&path, Path::new(&dep.exported_to)); + let dep_path = out_dir.as_ref().join(dep.output_path); + let rel_path = import_path(&path, &dep_path); writeln!( out, "import type {{ {} }} from {:?};", diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 295287512..512e07852 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -149,14 +149,24 @@ pub mod typelist; /// /// ### exporting /// Because Rusts procedural macros are evaluated before other compilation steps, TypeScript -/// bindings cannot be exported during compile time. +/// bindings __cannot__ be exported during compile time. +/// /// Bindings can be exported within a test, which ts-rs generates for you by adding `#[ts(export)]` -/// to a type you wish to export to a file. -/// If, for some reason, you need to do this during runtime, you can call [`TS::export`] yourself. -/// -/// **Note:** -/// Annotating a type with `#[ts(export)]` (or exporting it during runtime using -/// [`TS::export`]) will cause all of its dependencies to be exported as well. +/// to a type you wish to export to a file. +/// When `cargo test` is run, all types annotated with `#[ts(export)]` and all of their +/// dependencies will be written to `TS_RS_EXPORT_DIR`, or `./bindings` by default. +/// +/// For each individual type, path and filename within the output directory can be changed using +/// `#[ts(export_to = "...")]`. By default, the filename will be derived from the name of the type. +/// +/// If, for some reason, you need to do this during runtime or cannot use `#[ts(export)]`, bindings +/// can be exported manually: +/// +/// | Function | Includes Dependencies | To | +/// |-----------------------|-----------------------|--------------------| +/// | [`TS::export`] | ❌ | `TS_RS_EXPORT_DIR` | +/// | [`TS::export_all`] | ✔️ | `TS_RS_EXPORT_DIR` | +/// | [`TS::export_all_to`] | ✔️ | _custom_ | /// /// ### serde compatibility /// By default, the feature `serde-compat` is enabled. @@ -380,39 +390,95 @@ pub trait TS { deps } - /// Manually export this type to a file. - /// The output file can be specified by annotating the type with `#[ts(export_to = ".."]`. - /// By default, the filename will be derived from the types name. + /// Manually export this type to the filesystem. + /// To export this type together with all of its dependencies, use [`TS::export_all`]. + /// + /// # Automatic Exporting + /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be + /// exported automatically whenever `cargo test` is run. + /// In that case, there is no need to manually call this function. + /// + /// # Target Directory + /// The target directory to which the type will be exported may be changed by setting the + /// `TS_RS_EXPORT_DIR` environment variable. By default, `./bindings` will be used. + /// + /// To specify a target directory manually, use [`TS::export_all_to`], which also exports all + /// dependencies. /// - /// When a type is annotated with `#[ts(export)]`, it is exported automatically within a test. - /// This function is only usefull if you need to export the type outside of the context of a - /// test. + /// To alter the filename or path of the type within the target directory, + /// use `#[ts(export_to = "...")]`. fn export() -> Result<(), ExportError> where Self: 'static, { - export::export_type_with_dependencies::() + let path = Self::default_output_path() + .ok_or_else(std::any::type_name::) + .map_err(ExportError::CannotBeExported)?; + + export::export_to::(path) + } + + /// Manually export this type to the filesystem, together with all of its dependencies. + /// To export only this type, without its dependencies, use [`TS::export`]. + /// + /// # Automatic Exporting + /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be + /// exported automatically whenever `cargo test` is run. + /// In that case, there is no need to manually call this function. + /// + /// # Target Directory + /// The target directory to which the types will be exported may be changed by setting the + /// `TS_RS_EXPORT_DIR` environment variable. By default, `./bindings` will be used. + /// + /// To specify a target directory manually, use [`TS::export_all_to`]. + /// + /// To alter the filenames or paths of the types within the target directory, + /// use `#[ts(export_to = "...")]`. + fn export_all() -> Result<(), ExportError> + where + Self: 'static, + { + export::export_all_into::(&*export::default_out_dir()) } - - /// Manually export this type to a file with a file with the specified path. This - /// function will ignore the `#[ts(export_to = "..)]` attribute. - fn export_to(path: impl AsRef) -> Result<(), ExportError> - where - Self: 'static, + + /// Manually export this type into the given directory, together with all of its dependencies. + /// To export only this type, without its dependencies, use [`TS::export`]. + /// + /// Unlike [`TS::export_all`], this function disregards `TS_RS_EXPORT_DIR`, using the provided + /// directory instead. + /// + /// To alter the filenames or paths of the types within the target directory, + /// use `#[ts(export_to = "...")]`. + /// + /// # Automatic Exporting + /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be + /// exported automatically whenever `cargo test` is run. + /// In that case, there is no need to manually call this function. + fn export_all_to(out_dir: impl AsRef) -> Result<(), ExportError> + where + Self: 'static, { - export::export_type_to::(path) + export::export_all_into::(out_dir) } /// Manually generate bindings for this type, returning a [`String`]. - /// This function does not format the output, even if the `format` feature is enabled. + /// This function does not format the output, even if the `format` feature is enabled. TODO + /// + /// # Automatic Exporting + /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be + /// exported automatically whenever `cargo test` is run. + /// In that case, there is no need to manually call this function. fn export_to_string() -> Result where Self: 'static, { - export::export_type_to_string::() + export::export_to_string::() } - /// Returns the output path to where `T` should be exported. + /// Returns the output path to where `T` should be exported. + /// The returned path does _not_ include the base directory from `TS_RS_EXPORT_DIR`. + /// + /// To get the output path containing `TS_RS_EXPORT_DIR`, use [`TS::default_output_path`]. /// /// When deriving `TS`, the output path can be altered using `#[ts(export_to = "...")]`. /// See the documentation of [`TS`] for more details. @@ -422,9 +488,26 @@ pub trait TS { /// /// If `T` cannot be exported (e.g because it's a primitive type), this function will return /// `None`. - fn output_path() -> Option { + fn output_path() -> Option<&'static Path> { None } + + /// Returns the output path to where `T` should be exported. + /// + /// The output of this function depends on the environment variable `TS_RS_EXPORT_DIR`, which is + /// used as base directory. If it is not set, `./bindings` is used as default directory. + /// + /// To get the output path relative to `TS_RS_EXPORT_DIR` and without reading the environment + /// variable, use [`TS::output_path`]. + /// + /// When deriving `TS`, the output path can be altered using `#[ts(export_to = "...")]`. + /// See the documentation of [`TS`] for more details. + /// + /// If `T` cannot be exported (e.g because it's a primitive type), this function will return + /// `None`. + fn default_output_path() -> Option { + Some(export::default_out_dir().join(Self::output_path()?)) + } } /// A typescript type which is depended upon by other types. @@ -436,8 +519,9 @@ pub struct Dependency { /// Name of the type in TypeScript pub ts_name: String, /// Path to where the type would be exported. By default a filename is derived from the types - /// name, which can be customized with `#[ts(export_to = "..")]`. - pub exported_to: String, + /// name, which can be customized with `#[ts(export_to = "..")]`. + /// This path does _not_ include a base directory. + pub output_path: &'static Path, } impl Dependency { @@ -445,11 +529,11 @@ impl Dependency { /// If `T` is not exportable (meaning `T::EXPORT_TO` is `None`), this function will return /// `None` pub fn from_ty() -> Option { - let exported_to = T::output_path()?.to_str()?.to_owned(); + let output_path = T::output_path()?; Some(Dependency { type_id: TypeId::of::(), ts_name: T::ident(), - exported_to, + output_path, }) } } diff --git a/ts-rs/tests/docs.rs b/ts-rs/tests/docs.rs index 026df25f9..d989fa011 100644 --- a/ts-rs/tests/docs.rs +++ b/ts-rs/tests/docs.rs @@ -136,7 +136,7 @@ fn export_a() { ) }; - let actual_content = fs::read_to_string(A::output_path().unwrap()).unwrap(); + let actual_content = fs::read_to_string(A::default_output_path().unwrap()).unwrap(); assert_eq!(actual_content, expected_content); } @@ -182,7 +182,7 @@ fn export_b() { ) }; - let actual_content = fs::read_to_string(B::output_path().unwrap()).unwrap(); + let actual_content = fs::read_to_string(B::default_output_path().unwrap()).unwrap(); assert_eq!(actual_content, expected_content); } @@ -215,7 +215,7 @@ fn export_c() { ) }; - let actual_content = fs::read_to_string(C::output_path().unwrap()).unwrap(); + let actual_content = fs::read_to_string(C::default_output_path().unwrap()).unwrap(); assert_eq!(actual_content, expected_content); } @@ -247,7 +247,7 @@ fn export_d() { "export type D = null;" ) }; - let actual_content = fs::read_to_string(D::output_path().unwrap()).unwrap(); + let actual_content = fs::read_to_string(D::default_output_path().unwrap()).unwrap(); assert_eq!(actual_content, expected_content); } @@ -280,7 +280,7 @@ fn export_e() { ) }; - let actual_content = fs::read_to_string(E::output_path().unwrap()).unwrap(); + let actual_content = fs::read_to_string(E::default_output_path().unwrap()).unwrap(); assert_eq!(actual_content, expected_content); } @@ -328,7 +328,7 @@ fn export_f() { ) }; - let actual_content = fs::read_to_string(F::output_path().unwrap()).unwrap(); + let actual_content = fs::read_to_string(F::default_output_path().unwrap()).unwrap(); assert_eq!(actual_content, expected_content); } @@ -376,7 +376,7 @@ fn export_g() { ) }; - let actual_content = fs::read_to_string(G::output_path().unwrap()).unwrap(); + let actual_content = fs::read_to_string(G::default_output_path().unwrap()).unwrap(); assert_eq!(actual_content, expected_content); } diff --git a/ts-rs/tests/export_manually.rs b/ts-rs/tests/export_manually.rs index 8686a70a7..cdf111879 100644 --- a/ts-rs/tests/export_manually.rs +++ b/ts-rs/tests/export_manually.rs @@ -36,7 +36,7 @@ fn export_manually() { ) }; - let actual_content = fs::read_to_string(User::output_path().unwrap()).unwrap(); + let actual_content = fs::read_to_string(User::default_output_path().unwrap()).unwrap(); assert_eq!(actual_content, expected_content); } @@ -57,7 +57,7 @@ fn export_manually_dir() { ) }; - let actual_content = fs::read_to_string(UserDir::output_path().unwrap()).unwrap(); + let actual_content = fs::read_to_string(UserDir::default_output_path().unwrap()).unwrap(); assert_eq!(actual_content, expected_content); } diff --git a/ts-rs/tests/imports.rs b/ts-rs/tests/imports.rs index a1e8ec3e3..35cad1709 100644 --- a/ts-rs/tests/imports.rs +++ b/ts-rs/tests/imports.rs @@ -26,7 +26,7 @@ pub enum TestEnum { fn test_def() { // The only way to get access to how the imports look is to export the type and load the exported file TestEnum::export().unwrap(); - let text = std::fs::read_to_string(TestEnum::output_path().unwrap()).unwrap(); + let text = std::fs::read_to_string(TestEnum::default_output_path().unwrap()).unwrap(); let expected = match (cfg!(feature = "format"), cfg!(feature = "import-esm")) { (true, true) => concat!( diff --git a/ts-rs/tests/path_bug.rs b/ts-rs/tests/path_bug.rs index fc8cbd1fb..12ade1bf1 100644 --- a/ts-rs/tests/path_bug.rs +++ b/ts-rs/tests/path_bug.rs @@ -2,13 +2,13 @@ use ts_rs::TS; #[derive(TS)] -#[ts(export, export_to = "../ts-rs/path_bug/")] +#[ts(export, export_to = "path_bug/aaa/")] struct Foo { bar: Bar, } #[derive(TS)] -#[ts(export_to = "path_bug/aaa/")] +#[ts(export_to = "../bindings/path_bug/")] struct Bar { i: i32, } @@ -17,6 +17,6 @@ struct Bar { fn path_bug() { export_bindings_foo(); - assert!(Foo::output_path().unwrap().is_file()); - assert!(Bar::output_path().unwrap().is_file()); + assert!(Foo::default_output_path().unwrap().is_file()); + assert!(Bar::default_output_path().unwrap().is_file()); }