reflex/parsers/
tsconfig.rs1use anyhow::{Context, Result};
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::path::{Path, PathBuf};
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct PathAliasMap {
27 pub aliases: HashMap<String, Vec<String>>,
30 pub base_url: Option<String>,
32 pub config_dir: PathBuf,
34}
35
36#[derive(Debug, Deserialize)]
38struct CompilerOptions {
39 #[serde(rename = "baseUrl")]
40 base_url: Option<String>,
41 paths: Option<HashMap<String, Vec<String>>>,
42}
43
44#[derive(Debug, Deserialize)]
46struct TsConfig {
47 #[serde(rename = "compilerOptions")]
48 compiler_options: Option<CompilerOptions>,
49}
50
51impl PathAliasMap {
52 pub fn from_file(tsconfig_path: impl AsRef<Path>) -> Result<Self> {
54 let tsconfig_path = tsconfig_path.as_ref();
55 let content = std::fs::read_to_string(tsconfig_path).with_context(|| {
56 format!("Failed to read tsconfig.json: {}", tsconfig_path.display())
57 })?;
58
59 let config: TsConfig = json5::from_str(&content).with_context(|| {
61 format!("Failed to parse tsconfig.json: {}", tsconfig_path.display())
62 })?;
63
64 let config_dir = tsconfig_path
65 .parent()
66 .ok_or_else(|| anyhow::anyhow!("Invalid tsconfig.json path"))?
67 .to_path_buf();
68
69 let compiler_options = config.compiler_options.unwrap_or(CompilerOptions {
70 base_url: None,
71 paths: None,
72 });
73
74 Ok(Self {
75 aliases: compiler_options.paths.unwrap_or_default(),
76 base_url: compiler_options.base_url,
77 config_dir,
78 })
79 }
80
81 pub fn find_nearest_tsconfig(source_file: &Path) -> Option<PathBuf> {
86 let mut current_dir = source_file.parent()?;
87
88 loop {
89 let tsconfig_path = current_dir.join("tsconfig.json");
90 if tsconfig_path.exists() {
91 return Some(tsconfig_path);
92 }
93
94 let nuxt_tsconfig = current_dir.join(".nuxt/tsconfig.json");
96 if nuxt_tsconfig.exists() {
97 return Some(nuxt_tsconfig);
98 }
99
100 current_dir = current_dir.parent()?;
102 }
103 }
104
105 pub fn resolve_alias(&self, import_path: &str) -> Option<String> {
114 log::debug!(
115 " resolve_alias: trying to match '{}' against {} aliases",
116 import_path,
117 self.aliases.len()
118 );
119
120 for (alias_pattern, target_paths) in &self.aliases {
122 log::trace!(
123 " Checking alias pattern: {} => {:?}",
124 alias_pattern,
125 target_paths
126 );
127 if alias_pattern.ends_with("/*") {
129 let alias_prefix = alias_pattern.trim_end_matches("/*");
130
131 if import_path.starts_with(alias_prefix) {
133 let suffix = import_path.strip_prefix(alias_prefix).unwrap_or("");
137
138 if let Some(target_pattern) = target_paths.first() {
140 let resolved = if target_pattern.ends_with("/*") {
142 let target_prefix = target_pattern.trim_end_matches("/*");
143 format!("{}{}", target_prefix, suffix)
144 } else {
145 let clean_suffix = suffix.trim_start_matches('/');
149 if clean_suffix.is_empty() {
150 target_pattern.to_string()
151 } else {
152 format!("{}/{}", target_pattern, clean_suffix)
153 }
154 };
155
156 log::trace!(
157 "Resolved alias {} + {} => {}",
158 alias_pattern,
159 import_path,
160 resolved
161 );
162 return Some(resolved);
163 }
164 }
165 } else {
166 if import_path == alias_pattern
168 && let Some(target) = target_paths.first()
169 {
170 log::trace!("Resolved exact alias {} => {}", alias_pattern, target);
171 return Some(target.clone());
172 }
173 }
174 }
175
176 None
177 }
178
179 pub fn resolve_relative_to_config(&self, path: &str) -> PathBuf {
181 let base = if let Some(ref base_url) = self.base_url {
182 self.config_dir.join(base_url)
183 } else {
184 self.config_dir.clone()
185 };
186
187 let joined = base.join(path);
188
189 joined
193 .components()
194 .fold(PathBuf::new(), |mut acc, component| {
195 match component {
196 std::path::Component::CurDir => acc, std::path::Component::ParentDir => {
198 acc.pop(); acc
200 }
201 _ => {
202 acc.push(component);
203 acc
204 }
205 }
206 })
207 }
208}
209
210pub fn parse_all_tsconfigs(
218 root: &Path,
219) -> Result<std::collections::HashMap<PathBuf, PathAliasMap>> {
220 use ignore::WalkBuilder;
221 use std::collections::HashMap;
222
223 log::debug!("Starting tsconfig discovery in {}", root.display());
224 let mut tsconfigs = HashMap::new();
225 let mut file_count = 0;
226
227 for entry in WalkBuilder::new(root)
229 .follow_links(false)
230 .build()
231 .filter_map(|e| e.ok())
232 {
233 let path = entry.path();
234
235 if path.file_name().and_then(|n| n.to_str()) == Some("tsconfig.json") {
237 file_count += 1;
238 log::debug!(
239 "Found tsconfig.json file #{}: {}",
240 file_count,
241 path.display()
242 );
243
244 match PathAliasMap::from_file(path) {
246 Ok(alias_map) => {
247 let config_dir = alias_map.config_dir.clone();
249 log::debug!(
250 " Parsed successfully: base_url={:?}, {} aliases",
251 alias_map.base_url,
252 alias_map.aliases.len()
253 );
254 tsconfigs.insert(config_dir, alias_map);
255 }
256 Err(e) => {
257 log::warn!("Failed to parse tsconfig.json at {}: {}", path.display(), e);
258 }
259 }
260 }
261 }
262
263 log::debug!(
264 "Tsconfig discovery complete: found {} files, parsed {} successfully",
265 file_count,
266 tsconfigs.len()
267 );
268 Ok(tsconfigs)
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use std::fs;
275 use tempfile::TempDir;
276
277 #[test]
278 fn test_parse_tsconfig_with_paths() {
279 let temp = TempDir::new().unwrap();
280 let tsconfig_path = temp.path().join("tsconfig.json");
281
282 let tsconfig_content = r#"{
283 "compilerOptions": {
284 "baseUrl": ".",
285 "paths": {
286 "~/*": ["./src/*"],
287 "@packages/*": ["../../packages/*"]
288 }
289 }
290 }"#;
291
292 fs::write(&tsconfig_path, tsconfig_content).unwrap();
293
294 let alias_map = PathAliasMap::from_file(&tsconfig_path).unwrap();
295
296 assert_eq!(alias_map.base_url, Some(".".to_string()));
297 assert_eq!(alias_map.aliases.len(), 2);
298 assert!(alias_map.aliases.contains_key("~/*"));
299 assert!(alias_map.aliases.contains_key("@packages/*"));
300 }
301
302 #[test]
303 fn test_resolve_wildcard_alias() {
304 let temp = TempDir::new().unwrap();
305 let alias_map = PathAliasMap {
306 aliases: HashMap::from([(
307 "@packages/*".to_string(),
308 vec!["../../packages/*".to_string()],
309 )]),
310 base_url: Some(".".to_string()),
311 config_dir: temp.path().to_path_buf(),
312 };
313
314 let resolved = alias_map.resolve_alias("@packages/ui/stores/auth");
316 assert_eq!(resolved, Some("../../packages/ui/stores/auth".to_string()));
317 }
318
319 #[test]
320 fn test_resolve_exact_alias() {
321 let temp = TempDir::new().unwrap();
322 let alias_map = PathAliasMap {
323 aliases: HashMap::from([("~".to_string(), vec!["./src".to_string()])]),
324 base_url: None,
325 config_dir: temp.path().to_path_buf(),
326 };
327
328 let resolved = alias_map.resolve_alias("~");
330 assert_eq!(resolved, Some("./src".to_string()));
331 }
332
333 #[test]
334 fn test_no_match() {
335 let temp = TempDir::new().unwrap();
336 let alias_map = PathAliasMap {
337 aliases: HashMap::from([(
338 "@packages/*".to_string(),
339 vec!["../../packages/*".to_string()],
340 )]),
341 base_url: None,
342 config_dir: temp.path().to_path_buf(),
343 };
344
345 let resolved = alias_map.resolve_alias("./relative/path");
347 assert_eq!(resolved, None);
348 }
349
350 #[test]
351 fn test_find_nearest_tsconfig() {
352 let temp = TempDir::new().unwrap();
353
354 let src_dir = temp.path().join("src");
356 let components_dir = src_dir.join("components");
357 fs::create_dir_all(&components_dir).unwrap();
358
359 let tsconfig_path = temp.path().join("tsconfig.json");
361 fs::write(&tsconfig_path, "{}").unwrap();
362
363 let source_file = components_dir.join("Button.tsx");
365 fs::write(&source_file, "export const Button = () => {}").unwrap();
366
367 let found = PathAliasMap::find_nearest_tsconfig(&source_file);
369 assert_eq!(found, Some(tsconfig_path));
370 }
371
372 #[test]
373 fn test_resolve_relative_to_config() {
374 let temp = TempDir::new().unwrap();
375 let alias_map = PathAliasMap {
376 aliases: HashMap::new(),
377 base_url: Some("src".to_string()),
378 config_dir: temp.path().to_path_buf(),
379 };
380
381 let resolved = alias_map.resolve_relative_to_config("utils/helper.ts");
382 assert_eq!(resolved, temp.path().join("src/utils/helper.ts"));
383 }
384}