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

Feat: add default export directory override #195

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ jobs:
run: |
cargo t
tsc bindings/* --noEmit
- name: dependencies e2e test with default export env
working-directory: e2e/dependencies/consumer
run: |
TS_RS_EXPORT_DIR=custom-bindings cargo t
tsc custom-bindings/* --noEmit
e2e-workspace:
name: Run 'workspace' end-to-end test
runs-on: ubuntu-latest
Expand All @@ -27,6 +32,11 @@ jobs:
run: |
cargo t
tsc parent/bindings/* --noEmit
- name: workspace e2e with default export env
working-directory: e2e/workspace
run: |
TS_RS_EXPORT_DIR=custom-bindings cargo t
tsc parent/custom-bindings/* --noEmit

readme-up-to-date:
name: Check that README.md is up-to-date
Expand Down
3 changes: 2 additions & 1 deletion e2e/workspace/crate1/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use ts_rs::TS;

#[derive(TS)]
#[ts(export_to = "crate1/")]
pub struct Crate1 {
pub x: [[[i32; 128]; 128]; 128],
}
}
8 changes: 8 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,18 @@ impl DerivedTS {
}

fn into_impl(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.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.name)
}
};
Expand Down Expand Up @@ -95,6 +101,8 @@ impl DerivedTS {
quote! {
#impl_start {
const EXPORT_TO: Option<&'static str> = Some(#export_to);
#get_export_to

#docs

fn decl() -> String {
Expand Down
31 changes: 28 additions & 3 deletions ts-rs/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::{
fmt::Write,
path::{Component, Path, PathBuf},
sync::Mutex,
sync::OnceLock,
};

use thiserror::Error;
Expand Down Expand Up @@ -128,6 +129,28 @@ 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::name())),
)
}
}

/// 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 @@ -141,7 +164,8 @@ pub(crate) fn export_type_to_string<T: TS + ?Sized + 'static>() -> Result<String
fn output_path<T: TS + ?Sized>() -> Result<PathBuf, ExportError> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| ManifestDirNotSet)?;
let manifest_dir = Path::new(&manifest_dir);
let path = PathBuf::from(T::EXPORT_TO.ok_or(CannotBeExported(std::any::type_name::<T>()))?);
let path =
PathBuf::from(T::get_export_to().ok_or(CannotBeExported(std::any::type_name::<T>()))?);
Ok(manifest_dir.join(path))
}

Expand All @@ -160,7 +184,8 @@ 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 path = Path::new(T::EXPORT_TO.ok_or(CannotBeExported(std::any::type_name::<T>()))?);
let export_to = T::get_export_to().ok_or(CannotBeExported(std::any::type_name::<T>()))?;
let path = Path::new(&export_to);

let deps = T::dependencies();
let deduplicated_deps = deps
Expand All @@ -170,7 +195,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
22 changes: 19 additions & 3 deletions ts-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ pub use ts_rs_macros::TS;
pub use crate::export::ExportError;
use crate::typelist::TypeList;

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

#[cfg(feature = "chrono-impl")]
mod chrono;
mod export;
Expand Down Expand Up @@ -199,10 +203,18 @@ pub mod typelist;
///
/// - `#[ts(export)]`:
/// Generates a test which will export the type, by default to `bindings/<name>.ts` when running
/// `cargo test`
/// `cargo test`. The default base directory can be overridden with the `TS_RS_EXPORT_DIR` environment variable.
/// Adding the variable to a project's [config.toml](https://doc.rust-lang.org/cargo/reference/config.html#env) can
/// make it easier to manage.
/// ```toml
/// # <project-root>/.cargo/config.toml
/// [env]
/// TS_RS_EXPORT_DIR = { value = "<OVERRIDE_DIR>", relative = true }
/// ```
///
/// - `#[ts(export_to = "..")]`:
/// Specifies where the type should be exported to. Defaults to `bindings/<name>.ts`.
/// The `export_to` attribute will also override the `TS_RS_EXPORT_DIR` environment variable.
/// If the provided path ends in a trailing `/`, it is interpreted as a directory.
/// Note that you need to add the `export` attribute as well, in order to generate a test which exports the type.
///
Expand Down Expand Up @@ -271,6 +283,10 @@ pub trait TS {
const EXPORT_TO: Option<&'static str> = None;
const DOCS: Option<&'static str> = None;

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.
fn decl() -> String {
Expand Down Expand Up @@ -370,15 +386,15 @@ pub struct Dependency {
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: &'static str,
pub exported_to: String,
}

impl Dependency {
/// Constructs a [`Dependency`] from the given type `T`.
/// 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::EXPORT_TO?;
let exported_to = T::get_export_to()?;
Some(Dependency {
type_id: TypeId::of::<T>(),
ts_name: T::name(),
Expand Down
Loading