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_else(|| 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 if let Some(target) = target_paths.first() {
169 log::trace!("Resolved exact alias {} => {}", alias_pattern, target);
170 return Some(target.clone());
171 }
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 let normalized = joined
192 .components()
193 .fold(PathBuf::new(), |mut acc, component| {
194 match component {
195 std::path::Component::CurDir => acc, std::path::Component::ParentDir => {
197 acc.pop(); acc
199 }
200 _ => {
201 acc.push(component);
202 acc
203 }
204 }
205 });
206
207 normalized
208 }
209}
210
211pub fn parse_all_tsconfigs(
219 root: &Path,
220) -> Result<std::collections::HashMap<PathBuf, PathAliasMap>> {
221 use ignore::WalkBuilder;
222 use std::collections::HashMap;
223
224 log::debug!("Starting tsconfig discovery in {}", root.display());
225 let mut tsconfigs = HashMap::new();
226 let mut file_count = 0;
227
228 for entry in WalkBuilder::new(root)
230 .follow_links(false)
231 .build()
232 .filter_map(|e| e.ok())
233 {
234 let path = entry.path();
235
236 if path.file_name().and_then(|n| n.to_str()) == Some("tsconfig.json") {
238 file_count += 1;
239 log::debug!(
240 "Found tsconfig.json file #{}: {}",
241 file_count,
242 path.display()
243 );
244
245 match PathAliasMap::from_file(path) {
247 Ok(alias_map) => {
248 let config_dir = alias_map.config_dir.clone();
250 log::debug!(
251 " Parsed successfully: base_url={:?}, {} aliases",
252 alias_map.base_url,
253 alias_map.aliases.len()
254 );
255 tsconfigs.insert(config_dir, alias_map);
256 }
257 Err(e) => {
258 log::warn!("Failed to parse tsconfig.json at {}: {}", path.display(), e);
259 }
260 }
261 }
262 }
263
264 log::debug!(
265 "Tsconfig discovery complete: found {} files, parsed {} successfully",
266 file_count,
267 tsconfigs.len()
268 );
269 Ok(tsconfigs)
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use std::fs;
276 use tempfile::TempDir;
277
278 #[test]
279 fn test_parse_tsconfig_with_paths() {
280 let temp = TempDir::new().unwrap();
281 let tsconfig_path = temp.path().join("tsconfig.json");
282
283 let tsconfig_content = r#"{
284 "compilerOptions": {
285 "baseUrl": ".",
286 "paths": {
287 "~/*": ["./src/*"],
288 "@packages/*": ["../../packages/*"]
289 }
290 }
291 }"#;
292
293 fs::write(&tsconfig_path, tsconfig_content).unwrap();
294
295 let alias_map = PathAliasMap::from_file(&tsconfig_path).unwrap();
296
297 assert_eq!(alias_map.base_url, Some(".".to_string()));
298 assert_eq!(alias_map.aliases.len(), 2);
299 assert!(alias_map.aliases.contains_key("~/*"));
300 assert!(alias_map.aliases.contains_key("@packages/*"));
301 }
302
303 #[test]
304 fn test_resolve_wildcard_alias() {
305 let temp = TempDir::new().unwrap();
306 let alias_map = PathAliasMap {
307 aliases: HashMap::from([(
308 "@packages/*".to_string(),
309 vec!["../../packages/*".to_string()],
310 )]),
311 base_url: Some(".".to_string()),
312 config_dir: temp.path().to_path_buf(),
313 };
314
315 let resolved = alias_map.resolve_alias("@packages/ui/stores/auth");
317 assert_eq!(resolved, Some("../../packages/ui/stores/auth".to_string()));
318 }
319
320 #[test]
321 fn test_resolve_exact_alias() {
322 let temp = TempDir::new().unwrap();
323 let alias_map = PathAliasMap {
324 aliases: HashMap::from([("~".to_string(), vec!["./src".to_string()])]),
325 base_url: None,
326 config_dir: temp.path().to_path_buf(),
327 };
328
329 let resolved = alias_map.resolve_alias("~");
331 assert_eq!(resolved, Some("./src".to_string()));
332 }
333
334 #[test]
335 fn test_no_match() {
336 let temp = TempDir::new().unwrap();
337 let alias_map = PathAliasMap {
338 aliases: HashMap::from([(
339 "@packages/*".to_string(),
340 vec!["../../packages/*".to_string()],
341 )]),
342 base_url: None,
343 config_dir: temp.path().to_path_buf(),
344 };
345
346 let resolved = alias_map.resolve_alias("./relative/path");
348 assert_eq!(resolved, None);
349 }
350
351 #[test]
352 fn test_find_nearest_tsconfig() {
353 let temp = TempDir::new().unwrap();
354
355 let src_dir = temp.path().join("src");
357 let components_dir = src_dir.join("components");
358 fs::create_dir_all(&components_dir).unwrap();
359
360 let tsconfig_path = temp.path().join("tsconfig.json");
362 fs::write(&tsconfig_path, "{}").unwrap();
363
364 let source_file = components_dir.join("Button.tsx");
366 fs::write(&source_file, "export const Button = () => {}").unwrap();
367
368 let found = PathAliasMap::find_nearest_tsconfig(&source_file);
370 assert_eq!(found, Some(tsconfig_path));
371 }
372
373 #[test]
374 fn test_resolve_relative_to_config() {
375 let temp = TempDir::new().unwrap();
376 let alias_map = PathAliasMap {
377 aliases: HashMap::new(),
378 base_url: Some("src".to_string()),
379 config_dir: temp.path().to_path_buf(),
380 };
381
382 let resolved = alias_map.resolve_relative_to_config("utils/helper.ts");
383 assert_eq!(resolved, temp.path().join("src/utils/helper.ts"));
384 }
385}