Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make #[ts(export_to = "...")] relative to TS_RS_EXPORT_DIR #250

Merged
merged 11 commits into from
Mar 7, 2024
14 changes: 11 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ jobs:
working-directory: e2e/dependencies/consumer
run: |
TS_RS_EXPORT_DIR=custom-bindings cargo t
tsc custom-bindings/* --noEmit --noUnusedLocals --strict
shopt -s globstar
tsc custom-bindings/**/*.ts --noEmit --noUnusedLocals --strict
e2e-workspace:
name: Run 'workspace' end-to-end test
runs-on: ubuntu-latest
Expand All @@ -36,7 +37,8 @@ jobs:
working-directory: e2e/workspace
run: |
TS_RS_EXPORT_DIR=custom-bindings cargo t
tsc parent/custom-bindings/* --noEmit --noUnusedLocals --strict
shopt -s globstar
tsc parent/custom-bindings/**/*.ts --noEmit --noUnusedLocals --strict
e2e-example:
name: End-to-end test example
runs-on: ubuntu-latest
Expand All @@ -54,7 +56,8 @@ jobs:
working-directory: example
run: |
TS_RS_EXPORT_DIR=custom-bindings cargo t
tsc custom-bindings/* --noEmit --noUnusedLocals --strict
shopt -s globstar
tsc custom-bindings/**/*.ts --noEmit --noUnusedLocals --strict

readme-up-to-date:
name: Check that README.md is up-to-date
Expand All @@ -78,6 +81,11 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Test with export env
run: |
TS_RS_EXPORT_DIR=output cargo test --no-default-features
shopt -s globstar
tsc ts-rs/output/tests-out/**/*.ts --noEmit --noUnusedLocals --strict
- name: No features
run: |
cargo test --no-default-features
Expand Down
31 changes: 13 additions & 18 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,22 @@ struct DerivedTS {

impl DerivedTS {
fn into_impl(mut self, rust_ty: Ident, generics: Generics) -> TokenStream {
let mut get_export_to = quote! {};
let export_to = match &self.export_to {
Some(dirname) if dirname.ends_with('/') => {
format!("{}{}.ts", dirname, self.ts_name)
}
Some(filename) => filename.clone(),
None => {
get_export_to = quote! {
fn get_export_to() -> Option<String> {
ts_rs::__private::get_export_to_path::<Self>()
}
};
format!("bindings/{}.ts", self.ts_name)
}
};

let export = self
.export
.then(|| self.generate_export_test(&rust_ty, &generics));

let export_to = {
let path = match self.export_to.as_deref() {
Some(dirname) if dirname.ends_with('/') => format!("{}{}.ts", dirname, self.ts_name),
Some(filename) => filename.to_owned(),
None => format!("bindings/{}.ts", self.ts_name),
};

quote! {
const EXPORT_TO: Option<&'static str> = Some(#path);
}
};

let docs = match &*self.docs {
"" => None,
docs => Some(quote!(const DOCS: Option<&'static str> = Some(#docs);)),
Expand All @@ -67,13 +63,12 @@ impl DerivedTS {
quote! {
#impl_start {
#assoc_type
const EXPORT_TO: Option<&'static str> = Some(#export_to);
#export_to

fn ident() -> String {
#ident.to_owned()
}

#get_export_to
#docs
#name
#decl
Expand Down
53 changes: 21 additions & 32 deletions ts-rs/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{
collections::BTreeMap,
fmt::Write,
path::{Component, Path, PathBuf},
sync::{Mutex, OnceLock},
sync::Mutex,
};

use thiserror::Error;
Expand Down Expand Up @@ -87,7 +87,7 @@ mod recursive_export {
/// Export `T` to the file specified by the `#[ts(export_to = ..)]` attribute
pub(crate) fn export_type<T: TS + ?Sized + 'static>() -> Result<(), ExportError> {
let path = output_path::<T>()?;
export_type_to::<T, _>(&path)
export_type_to::<T, _>(path::absolute(path)?)
}

/// Export `T` to the file specified by the `path` argument.
Expand Down Expand Up @@ -124,30 +124,6 @@ pub(crate) fn export_type_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
Ok(())
}

#[doc(hidden)]
pub mod __private {
use super::*;

const EXPORT_DIR_ENV_VAR: &str = "TS_RS_EXPORT_DIR";
fn provided_default_dir() -> Option<&'static str> {
static EXPORT_TO: OnceLock<Option<String>> = OnceLock::new();
EXPORT_TO
.get_or_init(|| std::env::var(EXPORT_DIR_ENV_VAR).ok())
.as_deref()
}

/// Returns the path to where `T` should be exported using the `TS_RS_EXPORT_DIR` environment variable.
///
/// This should only be used by the TS derive macro; the `get_export_to` trait method should not
/// be overridden if the `#[ts(export_to = ..)]` attribute exists.
pub fn get_export_to_path<T: TS + ?Sized>() -> Option<String> {
provided_default_dir().map_or_else(
|| T::EXPORT_TO.map(ToString::to_string),
|path| Some(format!("{path}/{}.ts", T::ident())),
)
}
}

/// Returns the generated defintion for `T`.
pub(crate) fn export_type_to_string<T: TS + ?Sized + 'static>() -> Result<String, ExportError> {
let mut buffer = String::with_capacity(1024);
Expand All @@ -158,9 +134,16 @@ pub(crate) fn export_type_to_string<T: TS + ?Sized + 'static>() -> Result<String
}

/// Compute the output path to where `T` should be exported.
fn output_path<T: TS + ?Sized>() -> Result<PathBuf, ExportError> {
path::absolute(Path::new(
&T::get_export_to()
pub fn output_path<T: TS + ?Sized>() -> Result<PathBuf, ExportError> {
let path = std::env::var("TS_RS_EXPORT_DIR")
.ok()
.as_deref()
.map(Path::new)
.unwrap_or_else(|| Path::new("."))
.to_owned();

Ok(path.join(
T::EXPORT_TO
.ok_or_else(|| std::any::type_name::<T>())
.map_err(ExportError::CannotBeExported)?,
))
Expand All @@ -181,9 +164,15 @@ fn generate_decl<T: TS + ?Sized>(out: &mut String) {

/// Push an import statement for all dependencies of `T`
fn generate_imports<T: TS + ?Sized + 'static>(out: &mut String) -> Result<(), ExportError> {
let base = std::env::var("TS_RS_EXPORT_DIR")
.ok()
.as_deref()
.map(Path::new)
.unwrap_or_else(|| Path::new("."))
.to_owned();
let export_to =
T::get_export_to().ok_or(ExportError::CannotBeExported(std::any::type_name::<T>()))?;
let path = Path::new(&export_to);
T::EXPORT_TO.ok_or(ExportError::CannotBeExported(std::any::type_name::<T>()))?;
let path = base.join(export_to);
Comment on lines +167 to +175
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we re-use output_path here? Might be the case that we have to get rid of the path::absolute in output_path for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't even think we need to remove path::absolute, this path will be fed into diff_paths, which uses absolute paths as well

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right, perfect!


let deps = T::dependencies();
let deduplicated_deps = deps
Expand All @@ -193,7 +182,7 @@ fn generate_imports<T: TS + ?Sized + 'static>(out: &mut String) -> Result<(), Ex
.collect::<BTreeMap<_, _>>();

for (_, dep) in deduplicated_deps {
let rel_path = import_path(path, Path::new(&dep.exported_to));
let rel_path = import_path(&path, Path::new(&dep.exported_to));
writeln!(
out,
"import type {{ {} }} from {:?};",
Expand Down
15 changes: 7 additions & 8 deletions ts-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,10 @@ use std::{
path::{Path, PathBuf},
};

pub use export::output_path;
pub use ts_rs_macros::TS;

pub use crate::export::ExportError;
// Used in generated code. Not public API
#[doc(hidden)]
pub use crate::export::__private;
use crate::typelist::TypeList;

#[cfg(feature = "chrono-impl")]
Expand Down Expand Up @@ -289,6 +287,7 @@ pub trait TS {
/// ```
type WithoutGenerics: TS + ?Sized;

/// The path given to `#[ts(export_to = "...")]`
const EXPORT_TO: Option<&'static str> = None;
const DOCS: Option<&'static str> = None;

Expand All @@ -304,10 +303,6 @@ pub trait TS {
}
}

fn get_export_to() -> Option<String> {
Self::EXPORT_TO.map(ToString::to_string)
}

/// Declaration of this type, e.g. `interface User { user_id: number, ... }`.
/// This function will panic if the type has no declaration.
///
Expand Down Expand Up @@ -428,7 +423,11 @@ impl Dependency {
/// If `T` is not exportable (meaning `T::EXPORT_TO` is `None`), this function will return
/// `None`
pub fn from_ty<T: TS + 'static + ?Sized>() -> Option<Self> {
let exported_to = T::get_export_to()?;
let exported_to = output_path::<T>()
.ok()
.as_deref()
.and_then(Path::to_str)
.map(ToOwned::to_owned)?;
Some(Dependency {
type_id: TypeId::of::<T>(),
ts_name: T::ident(),
Expand Down
17 changes: 9 additions & 8 deletions ts-rs/tests/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

use std::{concat, fs};

use ts_rs::TS;
use ts_rs::{TS, output_path};


/* ============================================================================================== */

Expand Down Expand Up @@ -136,7 +137,7 @@ fn export_a() {
)
};

let actual_content = fs::read_to_string("tests-out/docs/A.ts").unwrap();
let actual_content = fs::read_to_string(output_path::<A>().unwrap()).unwrap();

assert_eq!(actual_content, expected_content);
}
Expand Down Expand Up @@ -182,7 +183,7 @@ fn export_b() {
)
};

let actual_content = fs::read_to_string("tests-out/docs/B.ts").unwrap();
let actual_content = fs::read_to_string(output_path::<B>().unwrap()).unwrap();

assert_eq!(actual_content, expected_content);
}
Expand Down Expand Up @@ -215,7 +216,7 @@ fn export_c() {
)
};

let actual_content = fs::read_to_string("tests-out/docs/C.ts").unwrap();
let actual_content = fs::read_to_string(output_path::<C>().unwrap()).unwrap();

assert_eq!(actual_content, expected_content);
}
Expand Down Expand Up @@ -247,7 +248,7 @@ fn export_d() {
"export type D = null;"
)
};
let actual_content = fs::read_to_string("tests-out/docs/D.ts").unwrap();
let actual_content = fs::read_to_string(output_path::<D>().unwrap()).unwrap();

assert_eq!(actual_content, expected_content);
}
Expand Down Expand Up @@ -280,7 +281,7 @@ fn export_e() {
)
};

let actual_content = fs::read_to_string("tests-out/docs/E.ts").unwrap();
let actual_content = fs::read_to_string(output_path::<E>().unwrap()).unwrap();

assert_eq!(actual_content, expected_content);
}
Expand Down Expand Up @@ -328,7 +329,7 @@ fn export_f() {
)
};

let actual_content = fs::read_to_string("tests-out/docs/F.ts").unwrap();
let actual_content = fs::read_to_string(output_path::<F>().unwrap()).unwrap();

assert_eq!(actual_content, expected_content);
}
Expand Down Expand Up @@ -376,7 +377,7 @@ fn export_g() {
)
};

let actual_content = fs::read_to_string("tests-out/docs/G.ts").unwrap();
let actual_content = fs::read_to_string(output_path::<G>().unwrap()).unwrap();

assert_eq!(actual_content, expected_content);
}
6 changes: 3 additions & 3 deletions ts-rs/tests/export_manually.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::{concat, fs};

use ts_rs::TS;
use ts_rs::{output_path, TS};

#[derive(TS)]
#[ts(export_to = "tests-out/export_manually/UserFile.ts")]
Expand Down Expand Up @@ -36,7 +36,7 @@ fn export_manually() {
)
};

let actual_content = fs::read_to_string("tests-out/export_manually/UserFile.ts").unwrap();
let actual_content = fs::read_to_string(output_path::<User>().unwrap()).unwrap();

assert_eq!(actual_content, expected_content);
}
Expand All @@ -57,7 +57,7 @@ fn export_manually_dir() {
)
};

let actual_content = fs::read_to_string("tests-out/export_manually/dir/UserDir.ts").unwrap();
let actual_content = fs::read_to_string(output_path::<UserDir>().unwrap()).unwrap();

assert_eq!(actual_content, expected_content);
}
13 changes: 11 additions & 2 deletions ts-rs/tests/imports.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#![allow(dead_code)]
use std::path::Path;

use ts_rs::TS;

#[derive(TS)]
Expand All @@ -25,7 +27,14 @@ 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::EXPORT_TO.unwrap()).unwrap();
let path = std::env::var("TS_RS_EXPORT_DIR")
.ok()
.as_deref()
.map(Path::new)
.unwrap_or_else(|| Path::new("."))
.to_owned()
.join(TestEnum::EXPORT_TO.unwrap());
let text = std::fs::read_to_string(&path).unwrap();

let expected = match (cfg!(feature = "format"), cfg!(feature = "import-esm")) {
(true, true) => concat!(
Expand Down Expand Up @@ -65,5 +74,5 @@ fn test_def() {
};

assert_eq!(text, expected);
std::fs::remove_file(TestEnum::EXPORT_TO.unwrap()).unwrap();
std::fs::remove_file(path).unwrap();
}
12 changes: 5 additions & 7 deletions ts-rs/tests/path_bug.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![allow(dead_code)]
use ts_rs::TS;
use ts_rs::{TS, output_path};

#[derive(TS)]
#[ts(export, export_to = "../ts-rs/tests-out/path_bug/")]
Expand All @@ -15,10 +15,8 @@ struct Bar {

#[test]
fn path_bug() {
Foo::export().unwrap();
Bar::export().unwrap();
export_bindings_foo();

let base = std::env::current_dir().unwrap();
assert!(base.join("./tests-out/path_bug/Foo.ts").is_file());
assert!(base.join("./tests-out/path_bug/aaa/Bar.ts").is_file());
}
assert!(output_path::<Foo>().unwrap().is_file());
assert!(output_path::<Bar>().unwrap().is_file());
}
Loading