use std::{
any::TypeId,
collections::BTreeMap,
fmt::Write,
path::{Component, Path, PathBuf},
};
use thiserror::Error;
use ExportError::*;
use crate::TS;
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,
#[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,
}
pub(crate) fn export_type<T: TS + ?Sized + 'static>() -> Result<(), ExportError> {
let path = output_path::<T>()?;
export_type_to::<T, _>(&path)
}
pub(crate) fn export_type_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
path: P,
) -> Result<(), ExportError> {
#[allow(unused_mut)]
let mut buffer = export_type_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| Formatting(e.to_string()))?
{
buffer = formatted;
}
}
if let Some(parent) = path.as_ref().parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path.as_ref(), buffer)?;
Ok(())
}
pub(crate) fn export_type_to_string<T: TS + ?Sized + 'static>() -> Result<String, ExportError> {
let mut buffer = String::with_capacity(1024);
buffer.push_str(NOTE);
generate_imports::<T>(&mut buffer)?;
generate_decl::<T>(&mut buffer);
Ok(buffer)
}
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)?);
Ok(manifest_dir.join(path))
}
fn generate_decl<T: TS + ?Sized>(out: &mut String) {
out.push_str("export ");
out.push_str(&T::decl());
}
fn generate_imports<T: TS + ?Sized + 'static>(out: &mut String) -> Result<(), ExportError> {
let path = Path::new(T::EXPORT_TO.ok_or(ExportError::CannotBeExported)?);
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 rel_path = import_path(path, Path::new(dep.exported_to));
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");
match rel_path.components().next() {
Some(Component::Normal(_)) => format!("./{}", rel_path.to_string_lossy()),
_ => rel_path.to_string_lossy().into(),
}
.trim_end_matches(".ts")
.to_owned()
}
fn diff_paths<P, B>(path: P, base: B) -> Option<PathBuf>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let base = base.as_ref();
if path.is_absolute() != base.is_absolute() {
if path.is_absolute() {
Some(PathBuf::from(path))
} else {
None
}
} else {
let mut ita = path.components();
let mut itb = base.components();
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(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(Component::CurDir)) => comps.push(a),
(Some(_), Some(Component::ParentDir)) => return None,
(Some(a), Some(_)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Some(comps.iter().map(|c| c.as_os_str()).collect())
}
}