1mod dts;
14mod ts_types;
15pub mod tsgo;
16mod wrapper;
17
18#[cfg(test)]
19mod tests;
20
21use std::collections::HashMap;
22use std::path::{Path, PathBuf};
23use std::process::Command;
24
25use crate::checker::Type;
26
27pub use dts::{DtsExport, parse_dts_exports};
29pub use ts_types::{ObjectField, TsType, ts_type_to_string};
30pub use tsgo::TsgoResolver;
31pub use wrapper::wrap_boundary_type;
32
33#[cfg(test)]
35#[allow(unused_imports)]
36use dts::{
37 parse_const_export, parse_dts_exports_from_str, parse_function_export, parse_interface_export,
38 parse_type_export,
39};
40#[cfg(test)]
41#[allow(unused_imports)]
42use ts_types::{find_matching_paren, parse_param_types, parse_type_str, split_at_top_level};
43
44#[derive(Debug, Clone)]
48pub struct ResolvedModule {
49 pub dts_path: PathBuf,
51 pub specifier: String,
53}
54
55#[derive(Debug, Clone)]
57pub struct ModuleExports {
58 pub exports: HashMap<String, Type>,
60 pub specifier: String,
62}
63
64pub fn resolve_module(specifier: &str, project_dir: &Path) -> Result<ResolvedModule, String> {
69 let tsconfig = crate::resolve::find_tsconfig_from(project_dir);
71
72 let mut cmd = Command::new("tsc");
73 cmd.current_dir(project_dir);
74
75 if let Some(tsconfig_path) = &tsconfig {
76 cmd.arg("--project").arg(tsconfig_path);
77 }
78
79 cmd.args(["--noEmit", "--traceResolution"]);
80
81 let probe_content = format!("import {{}} from \"{specifier}\";");
83 let probe_path = project_dir.join("__floe_probe__.ts");
84
85 if std::fs::write(&probe_path, &probe_content).is_err() {
86 return Err(format!(
87 "failed to create probe file for module '{specifier}'"
88 ));
89 }
90
91 cmd.arg(&probe_path);
92
93 let output = cmd.output().map_err(|e| {
94 let _ = std::fs::remove_file(&probe_path);
95 format!("failed to run tsc: {e}. Is TypeScript installed?")
96 })?;
97
98 let _ = std::fs::remove_file(&probe_path);
99
100 let stderr = String::from_utf8_lossy(&output.stderr);
101 let stdout = String::from_utf8_lossy(&output.stdout);
102 let combined = format!("{stdout}\n{stderr}");
103
104 parse_resolved_path(&combined, specifier)
108}
109
110fn parse_resolved_path(trace: &str, specifier: &str) -> Result<ResolvedModule, String> {
112 let success_marker = "was successfully resolved to '";
114 for line in trace.lines() {
115 if line.contains(&format!("Module name '{specifier}'"))
116 && let Some(start) = line.find(success_marker)
117 {
118 let rest = &line[start + success_marker.len()..];
119 if let Some(end) = rest.find("'.") {
120 let dts_path = &rest[..end];
121 return Ok(ResolvedModule {
122 dts_path: PathBuf::from(dts_path),
123 specifier: specifier.to_string(),
124 });
125 }
126 }
127 }
128
129 Err(format!(
132 "could not resolve module '{specifier}'. Make sure the package is installed (npm install)"
133 ))
134}
135
136pub fn resolve_and_wrap(specifier: &str, project_dir: &Path) -> Result<ModuleExports, String> {
141 let resolved = resolve_module(specifier, project_dir)?;
142 let dts_exports = parse_dts_exports(&resolved.dts_path)?;
143
144 let mut exports = HashMap::new();
145 for export in dts_exports {
146 let wrapped = wrap_boundary_type(&export.ts_type);
147 exports.insert(export.name, wrapped);
148 }
149
150 Ok(ModuleExports {
151 exports,
152 specifier: specifier.to_string(),
153 })
154}