Skip to main content

specta_typescript/
exporter.rs

1use std::{
2    borrow::Cow,
3    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
4    fmt,
5    ops::Deref,
6    panic::Location,
7    path::{Path, PathBuf},
8    sync::Arc,
9};
10
11use specta::{
12    Format, Types,
13    datatype::{DataType, Fields, NamedDataType, NamedReference, NamedReferenceType, Reference},
14};
15
16use crate::{Branded, Error, primitives, references};
17
18fn rust_type_path(ndt: &NamedDataType) -> Cow<'static, str> {
19    if ndt.module_path.is_empty() {
20        ndt.name.clone()
21    } else {
22        Cow::Owned(format!("{}::{}", ndt.module_path, ndt.name))
23    }
24}
25
26/// Allows configuring the format of the final types file
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
28pub enum Layout {
29    /// Produce a Typescript namespace for each Rust module
30    Namespaces,
31    /// Produce a dedicated file for each Rust module
32    Files,
33    /// Include the full module path in the types name but keep a flat structure.
34    ModulePrefixedName,
35    /// Flatten all of the types into a single file of types.
36    /// This mode doesn't support having multiple types with the same name.
37    #[default]
38    FlatFile,
39}
40
41impl fmt::Display for Layout {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(f, "{self:?}")
44    }
45}
46
47#[derive(Clone)]
48#[allow(clippy::type_complexity)]
49struct RuntimeFn(Arc<dyn Fn(FrameworkExporter) -> Result<Cow<'static, str>, Error> + Send + Sync>);
50
51impl fmt::Debug for RuntimeFn {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "RuntimeFn({:p})", self.0)
54    }
55}
56
57#[derive(Clone)]
58#[allow(clippy::type_complexity)]
59pub struct BrandedTypeImpl(
60    pub(crate)  Arc<
61        dyn for<'a> Fn(BrandedTypeExporter<'a>, &Branded) -> Result<Cow<'static, str>, Error>
62            + Send
63            + Sync,
64    >,
65);
66
67impl fmt::Debug for BrandedTypeImpl {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "BrandedTypeImpl({:p})", self.0)
70    }
71}
72
73/// Typescript language exporter.
74#[derive(Debug, Clone)]
75#[non_exhaustive]
76pub struct Exporter {
77    /// Custom header prepended to exported files.
78    pub header: Cow<'static, str>,
79    raw: Vec<Cow<'static, str>>,
80    framework_runtime: Option<RuntimeFn>,
81    pub(crate) branded_type_impl: Option<BrandedTypeImpl>,
82    framework_prelude: Cow<'static, str>,
83    /// Output layout mode for generated TypeScript.
84    pub layout: Layout,
85    pub(crate) jsdoc: bool,
86}
87
88impl Exporter {
89    // You should get this from either a [Typescript] or [JSDoc], not construct it directly.
90    pub(crate) fn default() -> Exporter {
91        Exporter {
92            header: Cow::Borrowed(""),
93            raw: Vec::new(),
94            framework_runtime: None,
95            branded_type_impl: None,
96            framework_prelude: Cow::Borrowed(
97                "// This file has been generated by Specta. Do not edit this file manually.",
98            ),
99            layout: Default::default(),
100            jsdoc: false,
101        }
102    }
103
104    /// Provide a prelude which is added to the start of all exported files.
105    pub fn framework_prelude(mut self, prelude: impl Into<Cow<'static, str>>) -> Self {
106        self.framework_prelude = prelude.into();
107        self
108    }
109
110    /// Add some custom Typescript or Javascript code that is exported as part of the bindings.
111    /// It's appending to the types file for single-file layouts or put in a root `index.{ts/js}` for multi-file.
112    ///
113    /// The closure is wrapped in [`specta::collect()`] to capture any referenced types.
114    /// Ensure you call `T::reference()` within the closure if you want an import to be created.
115    pub fn framework_runtime(
116        mut self,
117        builder: impl Fn(FrameworkExporter) -> Result<Cow<'static, str>, Error> + Send + Sync + 'static,
118    ) -> Self {
119        self.framework_runtime = Some(RuntimeFn(Arc::new(builder)));
120        self
121    }
122
123    /// Add raw Typescript or Javascript code that is exported as part of the bindings.
124    pub fn with_raw(mut self, raw: impl Into<Cow<'static, str>>) -> Self {
125        self.raw.push(raw.into());
126        self
127    }
128
129    /// Configure how `specta_typescript::branded!` types are rendered with exporter context.
130    ///
131    /// This callback receives both the branded payload and a [`BrandedTypeExporter`], allowing
132    /// you to call [`BrandedTypeExporter::inline`] / [`BrandedTypeExporter::reference`] while
133    /// preserving the current export configuration.
134    ///
135    /// # Examples
136    ///
137    /// `ts-brand` style:
138    /// ```rust
139    /// # use std::borrow::Cow;
140    /// # use specta_typescript::{Branded, Error, Typescript};
141    /// let exporter = Typescript::default().branded_type_impl(|ctx, branded| {
142    ///     let datatype = ctx.inline(branded.ty())?;
143    ///
144    ///     Ok(Cow::Owned(format!(
145    ///         "import(\"ts-brand\").Brand<{}, \"{}\">",
146    ///         datatype,
147    ///         branded.brand()
148    ///     )))
149    /// });
150    /// # let _ = exporter;
151    /// ```
152    ///
153    /// Effect style:
154    /// ```rust
155    /// # use std::borrow::Cow;
156    /// # use specta_typescript::{Branded, Error, Typescript};
157    /// let exporter = Typescript::default().branded_type_impl(|ctx, branded| {
158    ///     let datatype = ctx.inline(branded.ty())?;
159    ///
160    ///     Ok(Cow::Owned(format!(
161    ///         "{} & import(\"effect\").Brand.Brand<\"{}\">",
162    ///         datatype,
163    ///         branded.brand()
164    ///     )))
165    /// });
166    /// # let _ = exporter;
167    /// ```
168    pub fn branded_type_impl(
169        mut self,
170        builder: impl for<'a> Fn(BrandedTypeExporter<'a>, &Branded) -> Result<Cow<'static, str>, Error>
171        + Send
172        + Sync
173        + 'static,
174    ) -> Self {
175        self.branded_type_impl = Some(BrandedTypeImpl(Arc::new(builder)));
176        self
177    }
178
179    /// Configure a header for the file.
180    ///
181    /// This is perfect for configuring lint ignore rules or other file-level comments.
182    pub fn header(mut self, header: impl Into<Cow<'static, str>>) -> Self {
183        self.header = header.into();
184        self
185    }
186
187    /// Configure the bindings layout
188    pub fn layout(mut self, layout: Layout) -> Self {
189        self.layout = layout;
190        self
191    }
192
193    /// Export the files into a single string.
194    ///
195    /// Note: This returns an error if the format is `Format::Files`.
196    pub fn export(&self, types: &Types, format: impl Format) -> Result<String, Error> {
197        fn inner(exporter: Exporter, types: &Types, format: &dyn Format) -> Result<String, Error> {
198            let types = format_types(types, &format)?;
199            let types = types.as_ref();
200
201            if let Layout::Files = exporter.layout {
202                return Err(Error::export_requires_export_to(exporter.layout));
203            }
204            if let Layout::Namespaces = exporter.layout
205                && exporter.jsdoc
206            {
207                return Err(Error::jsdoc_namespaces_unsupported());
208            }
209
210            let mut out = render_file_header(&exporter)?;
211
212            let mut has_manually_exported_user_types = false;
213            let mut runtime = Ok(Cow::default());
214            if let Some(framework_runtime) = &exporter.framework_runtime {
215                runtime = (framework_runtime.0)(FrameworkExporter {
216                    exporter: &exporter,
217                    format: Some(&format),
218                    has_manually_exported_user_types: &mut has_manually_exported_user_types,
219                    files_root_types: "",
220                    types,
221                });
222            }
223            let runtime = render_runtime(&exporter, runtime?);
224
225            // Framework runtime
226            if !runtime.is_empty() {
227                out += "\n";
228            }
229            out += &runtime;
230            if !runtime.is_empty() {
231                out += "\n";
232            }
233
234            // User types (if not included in framework runtime)
235            if !has_manually_exported_user_types {
236                render_types(&mut out, &exporter, Some(&format), types, "")?;
237            }
238
239            Ok(out)
240        }
241
242        inner(self.clone(), types, &format)
243    }
244
245    /// Export the types to a specific file/folder.
246    ///
247    /// When configured when `format` is `Format::Files`, you must provide a directory path.
248    /// Otherwise, you must provide the path of a single file.
249    ///
250    pub fn export_to(
251        &self,
252        path: impl AsRef<Path>,
253        types: &Types,
254        format: impl Format,
255    ) -> Result<(), Error> {
256        fn inner(
257            exporter: Exporter,
258            path: &Path,
259            types: &Types,
260            format: &dyn Format,
261        ) -> Result<(), Error> {
262            let formatted_types = format_types(types, &format)?;
263            let types = formatted_types.as_ref();
264
265            if exporter.layout != Layout::Files {
266                let mut result = render_file_header(&exporter)?;
267
268                let mut has_manually_exported_user_types = false;
269                let mut runtime = Ok(Cow::default());
270                if let Some(framework_runtime) = &exporter.framework_runtime {
271                    runtime = (framework_runtime.0)(FrameworkExporter {
272                        exporter: &exporter,
273                        format: Some(&format),
274                        has_manually_exported_user_types: &mut has_manually_exported_user_types,
275                        files_root_types: "",
276                        types,
277                    });
278                }
279                let runtime = render_runtime(&exporter, runtime?);
280
281                if !runtime.is_empty() {
282                    result.push('\n');
283                    result.push_str(&runtime);
284                    result.push('\n');
285                }
286
287                if !has_manually_exported_user_types {
288                    render_types(&mut result, &exporter, Some(&format), types, "")?;
289                }
290
291                if let Some(parent) = path.parent() {
292                    std::fs::create_dir_all(parent)
293                        .map_err(|source| Error::create_dir(parent.to_path_buf(), source))?;
294                };
295                std::fs::write(path, result)
296                    .map_err(|source| Error::write_file(path.to_path_buf(), source))?;
297                return Ok(());
298            }
299
300            fn export(
301                exporter: &Exporter,
302                format: Option<&dyn Format>,
303                types: &Types,
304                module: &mut Module,
305                s: &mut String,
306                path: &Path,
307                files: &mut HashMap<PathBuf, String>,
308            ) -> Result<bool, Error> {
309                module.types.sort_by(|a, b| {
310                    a.name
311                        .cmp(&b.name)
312                        .then(a.module_path.cmp(&b.module_path))
313                        .then(a.location.cmp(&b.location))
314                });
315                let (rendered_types_result, referenced_types) =
316                    references::with_module_path(module.module_path.as_ref(), || {
317                        references::collect_references(|| {
318                            let mut rendered = String::new();
319                            let exports = render_flat_types(
320                                &mut rendered,
321                                exporter,
322                                format,
323                                types,
324                                module.types.iter().copied(),
325                                "",
326                            )?;
327                            Ok::<_, Error>((rendered, exports))
328                        })
329                    });
330                let (rendered_types, exports) = rendered_types_result?;
331
332                let import_paths = referenced_types
333                    .into_iter()
334                    .map(|r| reference_module_path(types, &r))
335                    .collect::<Result<Vec<_>, _>>()?
336                    .into_iter()
337                    .flatten()
338                    .filter(|module_path| module_path != module.module_path.as_ref())
339                    .collect::<BTreeSet<_>>();
340                if !import_paths.is_empty() {
341                    s.push('\n');
342                    s.push_str(&module_import_block(
343                        exporter,
344                        module.module_path.as_ref(),
345                        &import_paths,
346                    ));
347                }
348
349                if !import_paths.is_empty() && !rendered_types.is_empty() {
350                    s.push('\n');
351                }
352
353                s.push_str(&rendered_types);
354
355                for (name, module) in &mut module.children {
356                    // This doesn't account for `NamedDataType::requires_reference`
357                    // but we keep it for performance.
358                    if module.types.is_empty() && module.children.is_empty() {
359                        continue;
360                    }
361
362                    let mut path = path.join(name);
363                    let mut out = render_file_header(exporter)?;
364
365                    let has_types =
366                        export(exporter, format, types, module, &mut out, &path, files)?;
367                    if has_types {
368                        path.set_extension(if exporter.jsdoc { "js" } else { "ts" });
369                        files.insert(path, out);
370                    }
371                }
372
373                Ok(!exports.is_empty())
374            }
375
376            let mut files = HashMap::new();
377            let mut runtime_path = path.join("index");
378            runtime_path.set_extension(if exporter.jsdoc { "js" } else { "ts" });
379
380            let mut root_types = String::new();
381            export(
382                &exporter,
383                Some(&format),
384                types,
385                &mut build_module_graph(types),
386                &mut root_types,
387                path,
388                &mut files,
389            )?;
390
391            {
392                let mut has_manually_exported_user_types = false;
393                let mut framework_runtime = Cow::default();
394                let mut runtime_references = HashSet::new();
395                if let Some(runtime_fn) = &exporter.framework_runtime {
396                    let (runtime_result, referenced_types) =
397                        references::with_module_path("", || {
398                            references::collect_references(|| {
399                                (runtime_fn.0)(FrameworkExporter {
400                                    exporter: &exporter,
401                                    format: Some(&format),
402                                    has_manually_exported_user_types:
403                                        &mut has_manually_exported_user_types,
404                                    files_root_types: &root_types,
405                                    types,
406                                })
407                            })
408                        });
409                    framework_runtime = runtime_result?;
410                    runtime_references = referenced_types;
411                }
412                let runtime = render_runtime(&exporter, framework_runtime);
413
414                let should_export_user_types =
415                    !has_manually_exported_user_types && !root_types.is_empty();
416
417                if !runtime.is_empty() || should_export_user_types {
418                    files.insert(runtime_path, {
419                        let mut out = render_file_header(&exporter)?;
420                        let mut body = String::new();
421
422                        // Framework runtime
423                        if !runtime.is_empty() {
424                            body.push_str(&runtime);
425                        }
426
427                        // User types (if not included in framework runtime)
428                        if should_export_user_types {
429                            if !body.is_empty() {
430                                body.push('\n');
431                            }
432
433                            body.push_str(&root_types);
434                        }
435
436                        let import_paths = runtime_references
437                            .into_iter()
438                            .map(|r| reference_module_path(types, &r))
439                            .collect::<Result<Vec<_>, _>>()?
440                            .into_iter()
441                            .flatten()
442                            .filter(|module_path| !module_path.is_empty())
443                            .collect::<BTreeSet<_>>();
444
445                        let import_paths = import_paths
446                            .into_iter()
447                            .filter(|module_path| {
448                                !body.contains(&module_import_statement(&exporter, "", module_path))
449                            })
450                            .collect::<BTreeSet<_>>();
451
452                        if !import_paths.is_empty() {
453                            out.push('\n');
454                            out.push_str(&module_import_block(&exporter, "", &import_paths));
455                        }
456
457                        if !body.is_empty() {
458                            out.push('\n');
459                            if !import_paths.is_empty() {
460                                out.push('\n');
461                            }
462                            out.push_str(&body);
463                        }
464
465                        out
466                    });
467                }
468            }
469
470            match path.metadata() {
471                Ok(meta) if !meta.is_dir() => std::fs::remove_file(path).or_else(|source| {
472                    if source.kind() == std::io::ErrorKind::NotFound {
473                        Ok(())
474                    } else {
475                        Err(Error::remove_file(path.to_path_buf(), source))
476                    }
477                })?,
478                Ok(_) => {}
479                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
480                Err(source) => {
481                    return Err(Error::metadata(path.to_path_buf(), source));
482                }
483            }
484
485            for (path, content) in &files {
486                if let Some(parent) = path.parent() {
487                    std::fs::create_dir_all(parent)
488                        .map_err(|source| Error::create_dir(parent.to_path_buf(), source))?;
489                }
490                std::fs::write(path, content)
491                    .map_err(|source| Error::write_file(path.clone(), source))?;
492            }
493
494            cleanup_stale_files(path, &files, &exporter)?;
495
496            Ok(())
497        }
498
499        inner(self.clone(), path.as_ref(), types, &format)
500    }
501}
502
503fn reference_module_path(types: &Types, r: &NamedReference) -> Result<Option<String>, Error> {
504    match &r.inner {
505        NamedReferenceType::Reference { .. } => types
506            .get(r)
507            .map(|ndt| Some(ndt.module_path.as_ref().to_string()))
508            .ok_or_else(|| {
509                Error::dangling_named_reference("import resolution".to_string(), format!("{r:?}"))
510            }),
511        NamedReferenceType::Inline { .. } => Ok(None),
512        NamedReferenceType::Recursive(_) => types
513            .get(r)
514            .map(|ndt| Some(ndt.module_path.as_ref().to_string()))
515            .ok_or_else(|| {
516                Error::dangling_named_reference("import resolution".to_string(), format!("{r:?}"))
517            }),
518    }
519}
520
521fn format_types<'a>(types: &'a Types, format: &dyn Format) -> Result<Cow<'a, Types>, Error> {
522    Ok(
523        match format
524            .map_types(types)
525            .map_err(|err| Error::format("type graph formatter failed", err))?
526        {
527            Cow::Borrowed(_) => Cow::Borrowed(types),
528            Cow::Owned(types) => Cow::Owned(types),
529        },
530    )
531}
532
533fn render_runtime(exporter: &Exporter, framework_runtime: Cow<'static, str>) -> String {
534    let mut runtime = String::new();
535
536    if !framework_runtime.is_empty() {
537        runtime.push_str(&framework_runtime);
538    }
539
540    for raw in exporter.raw.iter().filter(|raw| !raw.is_empty()) {
541        if !runtime.is_empty() {
542            runtime.push('\n');
543        }
544        runtime.push_str(raw);
545    }
546
547    runtime
548}
549
550fn map_datatype_format(
551    format: Option<&dyn Format>,
552    types: &Types,
553    dt: &DataType,
554    path: &[Cow<'static, str>],
555) -> Result<DataType, Error> {
556    if matches!(dt, DataType::Generic(_)) {
557        return Ok(dt.clone());
558    }
559
560    fn contains_generic_reference(dt: &DataType) -> Result<bool, Error> {
561        Ok(match dt {
562            DataType::Primitive(_) => false,
563            DataType::List(list) => contains_generic_reference(&list.ty)?,
564            DataType::Map(map) => {
565                contains_generic_reference(map.key_ty())?
566                    || contains_generic_reference(map.value_ty())?
567            }
568            DataType::Nullable(inner) => contains_generic_reference(inner)?,
569            DataType::Struct(strct) => match &strct.fields {
570                Fields::Unit => false,
571                Fields::Unnamed(unnamed) => unnamed
572                    .fields
573                    .iter()
574                    .filter_map(|field| field.ty.as_ref())
575                    .try_fold(false, |found, ty| {
576                        Ok::<_, Error>(found || contains_generic_reference(ty)?)
577                    })?,
578                Fields::Named(named) => named
579                    .fields
580                    .iter()
581                    .filter_map(|(_, field)| field.ty.as_ref())
582                    .try_fold(false, |found, ty| {
583                        Ok::<_, Error>(found || contains_generic_reference(ty)?)
584                    })?,
585            },
586            DataType::Enum(enm) => enm.variants.iter().try_fold(false, |found, (_, variant)| {
587                let variant_found = match &variant.fields {
588                    Fields::Unit => false,
589                    Fields::Unnamed(unnamed) => unnamed
590                        .fields
591                        .iter()
592                        .filter_map(|field| field.ty.as_ref())
593                        .try_fold(false, |found, ty| {
594                            Ok::<_, Error>(found || contains_generic_reference(ty)?)
595                        })?,
596                    Fields::Named(named) => named
597                        .fields
598                        .iter()
599                        .filter_map(|(_, field)| field.ty.as_ref())
600                        .try_fold(false, |found, ty| {
601                            Ok::<_, Error>(found || contains_generic_reference(ty)?)
602                        })?,
603                };
604
605                Ok::<_, Error>(found || variant_found)
606            })?,
607            DataType::Tuple(tuple) => tuple.elements.iter().try_fold(false, |found, ty| {
608                Ok::<_, Error>(found || contains_generic_reference(ty)?)
609            })?,
610            DataType::Reference(Reference::Named(reference)) => match &reference.inner {
611                NamedReferenceType::Reference { generics, .. } => {
612                    generics.iter().try_fold(false, |found, (_, dt)| {
613                        Ok::<_, Error>(found || contains_generic_reference(dt)?)
614                    })?
615                }
616                NamedReferenceType::Inline { .. } => false,
617                NamedReferenceType::Recursive(_) => false,
618            },
619            DataType::Generic(_) => true,
620            DataType::Reference(Reference::Opaque(_)) => false,
621            DataType::Intersection(types) => types.iter().try_fold(false, |found, ty| {
622                Ok::<_, Error>(found || contains_generic_reference(ty)?)
623            })?,
624        })
625    }
626
627    if contains_generic_reference(dt)? {
628        let Some(format) = format else {
629            return map_datatype_format_children(None, types, dt.clone(), path);
630        };
631
632        match format.map_type(types, dt) {
633            Ok(Cow::Borrowed(dt)) => {
634                return map_datatype_format_children(Some(format), types, dt.clone(), path);
635            }
636            Ok(Cow::Owned(dt)) => {
637                return map_datatype_format_children(Some(format), types, dt, path);
638            }
639            Err(err) if is_unresolved_generic_format_error(err.as_ref()) => {
640                return map_datatype_format_children(Some(format), types, dt.clone(), path);
641            }
642            Err(err) => {
643                return Err(Error::format_at(
644                    "datatype formatter failed",
645                    path.join("."),
646                    err,
647                ));
648            }
649        }
650    }
651
652    let Some(format) = format else {
653        return Ok(dt.clone());
654    };
655
656    let mapped = format
657        .map_type(types, dt)
658        .map_err(|err| Error::format_at("datatype formatter failed", path.join("."), err))?;
659
660    match mapped {
661        Cow::Borrowed(dt) => map_datatype_format_children(Some(format), types, dt.clone(), path),
662        Cow::Owned(dt) => map_datatype_format_children(Some(format), types, dt, path),
663    }
664}
665
666fn is_unresolved_generic_format_error(err: &(dyn std::error::Error + 'static)) -> bool {
667    // The format trait currently erases its concrete error type, so this is the
668    // narrowest compatibility fallback available for formatters that reject
669    // unresolved generics before child mapping has substituted them.
670    err.to_string().contains("Unresolved generic reference")
671}
672
673fn map_datatype_format_children(
674    format: Option<&dyn Format>,
675    types: &Types,
676    mut dt: DataType,
677    path: &[Cow<'static, str>],
678) -> Result<DataType, Error> {
679    match &mut dt {
680        DataType::Primitive(_) => {}
681        DataType::List(list) => {
682            let child_path = format_path(path, "<list_item>");
683            *list.ty = map_datatype_format(format, types, &list.ty, &child_path)?;
684        }
685        DataType::Map(map) => {
686            let key_path = format_path(path, "<map_key>");
687            let value_path = format_path(path, "<map_value>");
688            let key = map_datatype_format(format, types, map.key_ty(), &key_path)?;
689            let value = map_datatype_format(format, types, map.value_ty(), &value_path)?;
690            map.set_key_ty(key);
691            map.set_value_ty(value);
692        }
693        DataType::Nullable(inner) => {
694            **inner = map_datatype_format(format, types, inner, path)?;
695        }
696        DataType::Struct(strct) => map_datatype_fields(format, types, &mut strct.fields, path)?,
697        DataType::Enum(enm) => {
698            for (variant_name, variant) in &mut enm.variants {
699                let variant_path = format_path(path, variant_name.clone());
700                map_datatype_fields(format, types, &mut variant.fields, &variant_path)?;
701            }
702        }
703        DataType::Tuple(tuple) => {
704            for (idx, element) in tuple.elements.iter_mut().enumerate() {
705                let element_path = format_path(path, idx.to_string());
706                *element = map_datatype_format(format, types, element, &element_path)?;
707            }
708        }
709        DataType::Intersection(types_) => {
710            for ty in types_ {
711                *ty = map_datatype_format(format, types, ty, path)?;
712            }
713        }
714        DataType::Reference(Reference::Named(reference)) => {
715            if let NamedReferenceType::Inline { dt, .. } = &mut reference.inner {
716                **dt = map_datatype_format(format, types, dt, path)?;
717            }
718
719            for (_, dt) in named_reference_generics_mut(reference) {
720                *dt = map_datatype_format(format, types, dt, path)?;
721            }
722        }
723        DataType::Generic(_) => {}
724        DataType::Reference(Reference::Opaque(reference)) => {
725            if let Some(branded) = reference.downcast_ref::<Branded>() {
726                dt = Reference::opaque(Branded::new(
727                    branded.brand().clone(),
728                    map_datatype_format(format, types, branded.ty(), path)?,
729                ))
730                .into();
731            }
732        }
733    }
734
735    Ok(dt)
736}
737
738fn format_path(
739    path: &[Cow<'static, str>],
740    segment: impl Into<Cow<'static, str>>,
741) -> Vec<Cow<'static, str>> {
742    let mut path = path.to_vec();
743    path.push(segment.into());
744    path
745}
746
747fn named_reference_generics_mut(
748    reference: &mut NamedReference,
749) -> &mut [(specta::datatype::Generic, DataType)] {
750    match &mut reference.inner {
751        NamedReferenceType::Reference { generics, .. } => generics,
752        NamedReferenceType::Inline { .. } | NamedReferenceType::Recursive(_) => &mut [],
753    }
754}
755
756fn map_datatype_fields(
757    format: Option<&dyn Format>,
758    types: &Types,
759    fields: &mut Fields,
760    path: &[Cow<'static, str>],
761) -> Result<(), Error> {
762    match fields {
763        Fields::Unit => {}
764        Fields::Unnamed(unnamed) => {
765            for (idx, field) in unnamed.fields.iter_mut().enumerate() {
766                if let Some(ty) = field.ty.as_mut() {
767                    let field_path = format_path(path, idx.to_string());
768                    *ty = map_datatype_format(format, types, ty, &field_path)?;
769                }
770            }
771        }
772        Fields::Named(named) => {
773            for (name, field) in &mut named.fields {
774                if let Some(ty) = field.ty.as_mut() {
775                    let field_path = format_path(path, name.clone());
776                    *ty = map_datatype_format(format, types, ty, &field_path)?;
777                }
778            }
779        }
780    }
781
782    Ok(())
783}
784
785fn map_named_datatype_format(
786    format: Option<&dyn Format>,
787    types: &Types,
788    ndt: &NamedDataType,
789) -> Result<NamedDataType, Error> {
790    let mut mapped = ndt.clone();
791    mapped.ty = ndt
792        .ty
793        .clone()
794        .map(|ty| map_datatype_format_children(format, types, ty, &[rust_type_path(ndt)]))
795        .transpose()
796        .map_err(|err| err.with_named_datatype(ndt))?;
797    Ok(mapped)
798}
799
800impl AsRef<Exporter> for Exporter {
801    fn as_ref(&self) -> &Exporter {
802        self
803    }
804}
805
806impl AsMut<Exporter> for Exporter {
807    fn as_mut(&mut self) -> &mut Exporter {
808        self
809    }
810}
811
812/// Reference to Typescript language exporter for branded type callbacks.
813pub struct BrandedTypeExporter<'a> {
814    pub(crate) exporter: &'a Exporter,
815    pub(crate) format: Option<&'a dyn Format>,
816    /// Collected types currently being exported.
817    pub types: &'a Types,
818}
819
820impl fmt::Debug for BrandedTypeExporter<'_> {
821    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
822        self.exporter.fmt(f)
823    }
824}
825
826impl AsRef<Exporter> for BrandedTypeExporter<'_> {
827    fn as_ref(&self) -> &Exporter {
828        self
829    }
830}
831
832impl Deref for BrandedTypeExporter<'_> {
833    type Target = Exporter;
834
835    fn deref(&self) -> &Self::Target {
836        self.exporter
837    }
838}
839
840impl BrandedTypeExporter<'_> {
841    /// [primitives::inline]
842    pub fn inline(&self, dt: &DataType) -> Result<String, Error> {
843        let mapped = map_datatype_format(self.format, self.types, dt, &[])?;
844        primitives::inline(self, self.types, &mapped)
845    }
846
847    /// Render a datatype while recursively preserving ordinary named references.
848    ///
849    /// See [`primitives::reference`] for details.
850    pub fn reference(&self, dt: &DataType) -> Result<String, Error> {
851        let mapped = map_datatype_format(self.format, self.types, dt, &[])?;
852        primitives::reference(self, self.types, &mapped)
853    }
854}
855
856/// Reference to Typescript language exporter for framework
857pub struct FrameworkExporter<'a> {
858    exporter: &'a Exporter,
859    format: Option<&'a dyn Format>,
860    has_manually_exported_user_types: &'a mut bool,
861    // For `Layout::Files` we need to inject the value
862    files_root_types: &'a str,
863    /// Collected types currently being exported.
864    pub types: &'a Types,
865}
866
867impl fmt::Debug for FrameworkExporter<'_> {
868    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
869        self.exporter.fmt(f)
870    }
871}
872
873impl AsRef<Exporter> for FrameworkExporter<'_> {
874    fn as_ref(&self) -> &Exporter {
875        self
876    }
877}
878
879impl Deref for FrameworkExporter<'_> {
880    type Target = Exporter;
881
882    fn deref(&self) -> &Self::Target {
883        self.exporter
884    }
885}
886
887impl FrameworkExporter<'_> {
888    /// Render the types within the [`Types`](specta::Types).
889    ///
890    /// This will only work if used within [`Exporter::framework_runtime`].
891    /// It allows frameworks to intersperse their user types into their runtime code.
892    pub fn render_types(&mut self) -> Result<Cow<'static, str>, Error> {
893        let mut s = String::new();
894        render_types(
895            &mut s,
896            self.exporter,
897            self.format,
898            self.types,
899            self.files_root_types,
900        )?;
901        *self.has_manually_exported_user_types = true;
902        Ok(Cow::Owned(s))
903    }
904
905    /// [primitives::inline]
906    pub fn inline(&self, dt: &DataType) -> Result<String, Error> {
907        let mapped = map_datatype_format(self.format, self.types, dt, &[])?;
908        primitives::inline(self, self.types, &mapped)
909    }
910
911    /// Render a datatype while recursively preserving ordinary named references.
912    ///
913    /// See [`primitives::reference`] for details.
914    pub fn reference(&self, dt: &DataType) -> Result<String, Error> {
915        let mapped = map_datatype_format(self.format, self.types, dt, &[])?;
916        primitives::reference(self, self.types, &mapped)
917    }
918
919    /// [primitives::export]
920    pub fn export<'a>(
921        &self,
922        ndts: impl Iterator<Item = &'a NamedDataType>,
923        indent: &'a str,
924    ) -> Result<String, Error> {
925        let mapped = ndts
926            .map(|ndt| map_named_datatype_format(self.format, self.types, ndt))
927            .collect::<Result<Vec<_>, _>>()?;
928        primitives::export(self, self.types, mapped.iter(), indent)
929    }
930}
931
932struct Module<'a> {
933    types: Vec<&'a NamedDataType>,
934    children: BTreeMap<&'a str, Module<'a>>,
935    module_path: Cow<'static, str>,
936}
937
938fn build_module_graph(types: &Types) -> Module<'_> {
939    types.into_unsorted_iter().fold(
940        Module {
941            types: Default::default(),
942            children: Default::default(),
943            module_path: Default::default(),
944        },
945        |mut ns, ndt| {
946            let path = &ndt.module_path;
947
948            if path.is_empty() {
949                ns.types.push(ndt);
950            } else {
951                let mut current = &mut ns;
952                let mut current_path = String::new();
953                for segment in path.split("::") {
954                    if !current_path.is_empty() {
955                        current_path.push_str("::");
956                    }
957                    current_path.push_str(segment);
958
959                    current = current.children.entry(segment).or_insert_with(|| Module {
960                        types: Default::default(),
961                        children: Default::default(),
962                        module_path: current_path.clone().into(),
963                    });
964                }
965
966                current.types.push(ndt);
967            }
968
969            ns
970        },
971    )
972}
973
974fn render_file_header(exporter: &Exporter) -> Result<String, Error> {
975    let mut out = exporter.header.to_string();
976    if !exporter.header.is_empty() {
977        out += "\n";
978    }
979
980    out += &exporter.framework_prelude;
981    if !exporter.framework_prelude.is_empty() {
982        out += "\n";
983    }
984
985    Ok(out)
986}
987
988fn render_types(
989    s: &mut String,
990    exporter: &Exporter,
991    format: Option<&dyn Format>,
992    types: &Types,
993    files_user_types: &str,
994) -> Result<(), Error> {
995    match exporter.layout {
996        Layout::Namespaces => {
997            fn has_renderable_content(module: &Module<'_>) -> bool {
998                module.types.iter().any(|ndt| ndt.ty.is_some())
999                    || module.children.values().any(has_renderable_content)
1000            }
1001
1002            fn export<'a>(
1003                exporter: &Exporter,
1004                format: Option<&dyn Format>,
1005                types: &Types,
1006                s: &mut String,
1007                module: impl ExactSizeIterator<Item = (&'a &'a str, &'a mut Module<'a>)>,
1008                depth: usize,
1009            ) -> Result<(), Error> {
1010                let namespace_indent = "\t".repeat(depth);
1011                let content_indent = "\t".repeat(depth + 1);
1012
1013                for (name, module) in module {
1014                    if !has_renderable_content(module) {
1015                        continue;
1016                    }
1017
1018                    s.push('\n');
1019                    s.push_str(&namespace_indent);
1020                    if depth != 0 && *name != "$specta$" {
1021                        s.push_str("export ");
1022                    }
1023                    s.push_str("namespace ");
1024                    s.push_str(name);
1025                    s.push_str(" {\n");
1026
1027                    // Types
1028                    module.types.sort_by(|a, b| {
1029                        a.name
1030                            .cmp(&b.name)
1031                            .then(a.module_path.cmp(&b.module_path))
1032                            .then(a.location.cmp(&b.location))
1033                    });
1034                    render_flat_types(
1035                        s,
1036                        exporter,
1037                        format,
1038                        types,
1039                        module.types.iter().copied(),
1040                        &content_indent,
1041                    )?;
1042
1043                    // Namespaces
1044                    export(
1045                        exporter,
1046                        format,
1047                        types,
1048                        s,
1049                        module.children.iter_mut(),
1050                        depth + 1,
1051                    )?;
1052
1053                    s.push_str(&namespace_indent);
1054                    s.push_str("}\n");
1055                }
1056
1057                Ok(())
1058            }
1059
1060            let mut module = build_module_graph(types);
1061
1062            let reexports = {
1063                let mut reexports = String::new();
1064                for name in module
1065                    .children
1066                    .iter()
1067                    .filter_map(|(name, module)| has_renderable_content(module).then_some(*name))
1068                    .chain(
1069                        module
1070                            .types
1071                            .iter()
1072                            .filter(|ndt| ndt.ty.is_some())
1073                            .map(|ndt| ndt.name.as_ref()),
1074                    )
1075                {
1076                    reexports.push_str("export import ");
1077                    reexports.push_str(name);
1078                    reexports.push_str(" = $s$.");
1079                    reexports.push_str(name);
1080                    reexports.push_str(";\n");
1081                }
1082                reexports
1083            };
1084
1085            export(
1086                exporter,
1087                format,
1088                types,
1089                s,
1090                [(&"$s$", &mut module)].into_iter(),
1091                0,
1092            )?;
1093            s.push_str(&reexports);
1094        }
1095        Layout::ModulePrefixedName | Layout::FlatFile => {
1096            render_flat_types(s, exporter, format, types, types.into_sorted_iter(), "")?;
1097        }
1098        // The types will get their own files
1099        // So we keep the user types empty for easy downstream detection.
1100        Layout::Files => {
1101            if !files_user_types.is_empty() {
1102                s.push_str(files_user_types);
1103            }
1104        }
1105    }
1106
1107    Ok(())
1108}
1109
1110// Implementation of `Layout::ModulePrefixedName | Layout::FlatFile`,
1111// but is used by `Layout::Namespace` and `Layout::Files`
1112fn render_flat_types<'a>(
1113    s: &mut String,
1114    exporter: &Exporter,
1115    format: Option<&dyn Format>,
1116    types: &Types,
1117    ndts: impl ExactSizeIterator<Item = &'a NamedDataType>,
1118    indent: &str,
1119) -> Result<HashMap<String, Location<'static>>, Error> {
1120    let mut exports = HashMap::with_capacity(ndts.len());
1121
1122    let ndts = ndts
1123        .filter(|ndt| ndt.ty.is_some())
1124        .map(|ndt| {
1125            let export_name = exported_type_name(exporter, ndt);
1126            if let Some(other) = exports.insert(export_name.to_string(), ndt.location) {
1127                return Err(Error::duplicate_type_name(export_name, ndt.location, other));
1128            }
1129
1130            Ok(ndt)
1131        })
1132        .collect::<Result<Vec<_>, _>>()?;
1133
1134    primitives::export_internal(s, exporter, format, types, ndts.into_iter(), indent)?;
1135
1136    Ok(exports)
1137}
1138
1139/// Collect all TypeScript/JavaScript files in a directory recursively
1140fn collect_existing_files(root: &Path) -> Result<HashSet<PathBuf>, Error> {
1141    if !root.exists() {
1142        return Ok(HashSet::new());
1143    }
1144
1145    let mut files = HashSet::new();
1146    let entries =
1147        std::fs::read_dir(root).map_err(|source| Error::read_dir(root.to_path_buf(), source))?;
1148    for entry in entries {
1149        let entry = entry.map_err(|source| Error::read_dir(root.to_path_buf(), source))?;
1150        let path = entry.path();
1151        let file_type = entry
1152            .file_type()
1153            .map_err(|source| Error::metadata(path.clone(), source))?;
1154
1155        if file_type.is_symlink() {
1156            continue;
1157        }
1158
1159        if file_type.is_dir() {
1160            files.extend(collect_existing_files(&path)?);
1161        } else if matches!(path.extension().and_then(|e| e.to_str()), Some("ts" | "js")) {
1162            files.insert(path);
1163        }
1164    }
1165
1166    Ok(files)
1167}
1168
1169fn is_generated_specta_file(path: &Path, exporter: &Exporter) -> Result<bool, Error> {
1170    match std::fs::read_to_string(path) {
1171        Ok(contents) => Ok((!exporter.framework_prelude.is_empty()
1172            && contents.contains(exporter.framework_prelude.as_ref()))
1173            || contents.contains("generated by Specta")),
1174        Err(err) if err.kind() == std::io::ErrorKind::InvalidData => Ok(false),
1175        Err(source) => Err(Error::read_file(path.to_path_buf(), source)),
1176    }
1177}
1178
1179/// Remove empty directories recursively, stopping at the root
1180fn remove_empty_dirs(path: &Path, root: &Path) -> Result<(), Error> {
1181    let entries =
1182        std::fs::read_dir(path).map_err(|source| Error::read_dir(path.to_path_buf(), source))?;
1183    for entry in entries {
1184        let entry = entry.map_err(|source| Error::read_dir(path.to_path_buf(), source))?;
1185        let entry_path = entry.path();
1186        let file_type = entry
1187            .file_type()
1188            .map_err(|source| Error::metadata(entry_path.clone(), source))?;
1189        if file_type.is_symlink() {
1190            continue;
1191        }
1192        if file_type.is_dir() {
1193            remove_empty_dirs(&entry_path, root)?;
1194        }
1195    }
1196
1197    let is_empty = path
1198        .read_dir()
1199        .map_err(|source| Error::read_dir(path.to_path_buf(), source))?
1200        .next()
1201        .is_none();
1202
1203    if path != root && is_empty {
1204        match std::fs::remove_dir(path) {
1205            Ok(()) => {}
1206            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
1207            Err(source) => {
1208                return Err(Error::remove_dir(path.to_path_buf(), source));
1209            }
1210        }
1211    }
1212    Ok(())
1213}
1214
1215/// Delete stale files and clean up empty directories
1216fn cleanup_stale_files(
1217    root: &Path,
1218    current_files: &HashMap<PathBuf, String>,
1219    exporter: &Exporter,
1220) -> Result<(), Error> {
1221    if !root.exists() {
1222        return Ok(());
1223    }
1224
1225    for path in collect_existing_files(root)? {
1226        if current_files.contains_key(&path) || !is_generated_specta_file(&path, exporter)? {
1227            continue;
1228        }
1229
1230        std::fs::remove_file(&path).or_else(|source| {
1231            if source.kind() == std::io::ErrorKind::NotFound {
1232                Ok(())
1233            } else {
1234                Err(Error::remove_file(path.clone(), source))
1235            }
1236        })?;
1237    }
1238
1239    remove_empty_dirs(root, root)?;
1240
1241    Ok(())
1242}
1243
1244fn exported_type_name(exporter: &Exporter, ndt: &NamedDataType) -> Cow<'static, str> {
1245    match exporter.layout {
1246        Layout::ModulePrefixedName => {
1247            let mut s = ndt.module_path.split("::").collect::<Vec<_>>().join("_");
1248            if !s.is_empty() {
1249                s.push('_');
1250            }
1251            s.push_str(&ndt.name);
1252            Cow::Owned(s)
1253        }
1254        _ => ndt.name.clone(),
1255    }
1256}
1257
1258pub(crate) fn module_alias(module_path: &str) -> String {
1259    if module_path.is_empty() {
1260        "$root".to_string()
1261    } else {
1262        module_path.split("::").collect::<Vec<_>>().join("$")
1263    }
1264}
1265
1266fn module_import_statement(
1267    exporter: &Exporter,
1268    from_module_path: &str,
1269    to_module_path: &str,
1270) -> String {
1271    let import_keyword = if exporter.jsdoc {
1272        "import"
1273    } else {
1274        "import type"
1275    };
1276
1277    format!(
1278        "{} * as {} from \"{}\";",
1279        import_keyword,
1280        module_alias(to_module_path),
1281        module_import_path(from_module_path, to_module_path)
1282    )
1283}
1284
1285fn module_import_block(
1286    exporter: &Exporter,
1287    from_module_path: &str,
1288    import_paths: &BTreeSet<String>,
1289) -> String {
1290    if exporter.jsdoc {
1291        let mut out = String::from("/**\n");
1292
1293        for module_path in import_paths {
1294            out.push_str(" * @typedef {import(\"");
1295            out.push_str(&module_import_path(from_module_path, module_path));
1296            out.push_str("\")} ");
1297            out.push_str(&module_alias(module_path));
1298            out.push('\n');
1299        }
1300
1301        out.push_str(" */");
1302        out
1303    } else {
1304        import_paths
1305            .iter()
1306            .map(|module_path| module_import_statement(exporter, from_module_path, module_path))
1307            .collect::<Vec<_>>()
1308            .join("\n")
1309    }
1310}
1311
1312fn module_import_path(from_module_path: &str, to_module_path: &str) -> String {
1313    fn module_file_segments(module_path: &str) -> Vec<&str> {
1314        if module_path.is_empty() {
1315            vec!["index"]
1316        } else {
1317            module_path.split("::").collect()
1318        }
1319    }
1320
1321    let from_file_segments = module_file_segments(from_module_path);
1322    let from_dir_segments = &from_file_segments[..from_file_segments.len() - 1];
1323    let to_file_segments = module_file_segments(to_module_path);
1324
1325    let shared = from_dir_segments
1326        .iter()
1327        .zip(to_file_segments.iter())
1328        .take_while(|(a, b)| a == b)
1329        .count();
1330
1331    let mut relative_parts = Vec::new();
1332    relative_parts.extend(std::iter::repeat_n(
1333        "..",
1334        from_dir_segments.len().saturating_sub(shared),
1335    ));
1336    relative_parts.extend(to_file_segments.iter().skip(shared).copied());
1337
1338    if relative_parts
1339        .first()
1340        .is_none_or(|v| *v != "." && *v != "..")
1341    {
1342        relative_parts.insert(0, ".");
1343    }
1344
1345    relative_parts.join("/")
1346}