use std::{
any::TypeId,
borrow::Cow,
collections::BTreeMap,
fmt::Write,
fs::File,
path::{Component, Path, PathBuf},
sync::Mutex,
};
pub(crate) use recursive_export::export_all_into;
use thiserror::Error;
use crate::TS;
mod path;
const NOTE: &str = "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n";
#[derive(Error, Debug)]
pub enum ExportError {
#[error("this type cannot be exported")]
CannotBeExported(&'static str),
#[cfg(feature = "format")]
#[error("an error occurred while formatting the generated typescript output")]
Formatting(String),
#[error("an error occurred while performing IO")]
Io(#[from] std::io::Error),
#[error("the environment variable CARGO_MANIFEST_DIR is not set")]
ManifestDirNotSet,
}
mod recursive_export {
use std::{any::TypeId, collections::HashSet, path::Path};
use super::export_into;
use crate::{
typelist::{TypeList, TypeVisitor},
ExportError, TS,
};
pub(crate) fn export_all_into<T: TS + ?Sized + 'static>(
out_dir: impl AsRef<Path>,
) -> Result<(), ExportError> {
let mut seen = HashSet::new();
export_recursive::<T>(&mut seen, out_dir)
}
struct Visit<'a> {
seen: &'a mut HashSet<TypeId>,
out_dir: &'a Path,
error: Option<ExportError>,
}
impl<'a> TypeVisitor for Visit<'a> {
fn visit<T: TS + 'static + ?Sized>(&mut self) {
if self.error.is_some() || T::output_path().is_none() {
return;
}
self.error = export_recursive::<T>(self.seen, self.out_dir).err();
}
}
fn export_recursive<T: TS + ?Sized + 'static>(
seen: &mut HashSet<TypeId>,
out_dir: impl AsRef<Path>,
) -> Result<(), ExportError> {
if !seen.insert(TypeId::of::<T>()) {
return Ok(());
}
let out_dir = out_dir.as_ref();
export_into::<T>(out_dir)?;
let mut visitor = Visit {
seen,
out_dir,
error: None,
};
T::dependency_types().for_each(&mut visitor);
if let Some(e) = visitor.error {
Err(e)
} else {
Ok(())
}
}
}
pub(crate) fn export_into<T: TS + ?Sized + 'static>(
out_dir: impl AsRef<Path>,
) -> Result<(), ExportError> {
let path = T::output_path()
.ok_or_else(std::any::type_name::<T>)
.map_err(ExportError::CannotBeExported)?;
let path = out_dir.as_ref().join(path);
export_to::<T, _>(path::absolute(path)?)
}
pub(crate) fn export_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
path: P,
) -> Result<(), ExportError> {
static FILE_LOCK: Mutex<()> = Mutex::new(());
#[allow(unused_mut)]
let mut buffer = export_to_string::<T>()?;
#[cfg(feature = "format")]
{
use dprint_plugin_typescript::{configuration::ConfigurationBuilder, format_text};
let fmt_cfg = ConfigurationBuilder::new().deno().build();
if let Some(formatted) = format_text(path.as_ref(), &buffer, &fmt_cfg)
.map_err(|e| ExportError::Formatting(e.to_string()))?
{
buffer = formatted;
}
}
if let Some(parent) = path.as_ref().parent() {
std::fs::create_dir_all(parent)?;
}
let lock = FILE_LOCK.lock().unwrap();
{
use std::io::Write;
let mut file = File::create(path)?;
file.write_all(buffer.as_bytes())?;
file.sync_data()?;
}
drop(lock);
Ok(())
}
pub(crate) fn export_to_string<T: TS + ?Sized + 'static>() -> Result<String, ExportError> {
let mut buffer = String::with_capacity(1024);
buffer.push_str(NOTE);
generate_imports::<T::WithoutGenerics>(&mut buffer, default_out_dir())?;
generate_decl::<T>(&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)),
}
}
fn generate_decl<T: TS + ?Sized>(out: &mut String) {
let docs = &T::DOCS;
if let Some(docs) = docs {
out.push_str(docs);
}
out.push_str("export ");
out.push_str(&T::decl());
}
fn generate_imports<T: TS + ?Sized + 'static>(
out: &mut String,
out_dir: impl AsRef<Path>,
) -> Result<(), ExportError> {
let path = T::output_path()
.ok_or_else(std::any::type_name::<T>)
.map_err(ExportError::CannotBeExported)?;
let path = out_dir.as_ref().join(path);
let deps = T::dependencies();
let deduplicated_deps = deps
.iter()
.filter(|dep| dep.type_id != TypeId::of::<T>())
.map(|dep| (&dep.ts_name, dep))
.collect::<BTreeMap<_, _>>();
for (_, dep) in deduplicated_deps {
let dep_path = out_dir.as_ref().join(dep.output_path);
let rel_path = import_path(&path, &dep_path);
writeln!(
out,
"import type {{ {} }} from {:?};",
&dep.ts_name, rel_path
)
.unwrap();
}
writeln!(out).unwrap();
Ok(())
}
fn import_path(from: &Path, import: &Path) -> String {
let rel_path =
diff_paths(import, from.parent().unwrap()).expect("failed to calculate import path");
let path = match rel_path.components().next() {
Some(Component::Normal(_)) => format!("./{}", rel_path.to_string_lossy()),
_ => rel_path.to_string_lossy().into(),
};
let path_without_extension = path.trim_end_matches(".ts");
if cfg!(feature = "import-esm") {
format!("{}.js", path_without_extension)
} else {
path_without_extension.to_owned()
}
}
fn diff_paths<P, B>(path: P, base: B) -> Result<PathBuf, ExportError>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
use Component as C;
let path = path::absolute(path)?;
let base = path::absolute(base)?;
let mut ita = path.components();
let mut itb = base.components();
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(Some(C::ParentDir | C::CurDir), _) | (_, Some(C::ParentDir | C::CurDir)) => {
unreachable!(
"The paths have been cleaned, no no '.' or '..' components are present"
)
}
(None, None) => break,
(Some(a), None) => {
comps.push(a);
comps.extend(ita.by_ref());
break;
}
(None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
(Some(a), Some(_)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Ok(comps.iter().map(|c| c.as_os_str()).collect())
}