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