fob_graph/analysis/resolver/
mod.rs1mod algorithm;
7mod aliases;
8mod extensions;
9
10pub use algorithm::{is_external, resolve_local, resolve_with_alias};
11pub use aliases::resolve_path_alias;
12pub use extensions::{EXTENSIONS, resolve_with_extensions};
13
14use std::path::{Path, PathBuf};
15
16use crate::runtime::{Runtime, RuntimeError};
17
18use crate::analysis::config::{AnalyzerConfig, ResolveResult};
19
20pub struct ModuleResolver {
22 config: AnalyzerConfig,
23}
24
25impl ModuleResolver {
26 pub fn new(config: AnalyzerConfig) -> Self {
28 Self { config }
29 }
30
31 pub async fn resolve(
40 &self,
41 specifier: &str,
42 from: &Path,
43 runtime: &dyn Runtime,
44 ) -> Result<ResolveResult, RuntimeError> {
45 if algorithm::is_external(specifier, &self.config.external) {
47 return Ok(ResolveResult::External(specifier.to_string()));
48 }
49
50 let cwd = self.get_cwd(runtime)?;
52 if let Some(resolved) = algorithm::resolve_with_alias(
53 specifier,
54 from,
55 cwd.as_path(),
56 &self.config.path_aliases,
57 runtime,
58 )
59 .await?
60 {
61 return Ok(resolved);
62 }
63
64 if specifier.starts_with('.') || specifier.starts_with('/') {
66 return algorithm::resolve_local(specifier, from, self.config.cwd.as_deref(), runtime)
68 .await;
69 }
70
71 Ok(ResolveResult::External(specifier.to_string()))
73 }
74
75 pub fn get_cwd(&self, runtime: &dyn Runtime) -> Result<PathBuf, RuntimeError> {
77 if let Some(ref cwd) = self.config.cwd {
78 Ok(cwd.clone())
79 } else {
80 runtime.get_cwd()
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::runtime::RuntimeError;
89 use std::path::PathBuf;
90
91 #[derive(Debug)]
93 struct MockRuntime {
94 files: Vec<PathBuf>,
95 }
96
97 #[cfg(not(target_family = "wasm"))]
98 #[async_trait::async_trait]
99 impl Runtime for MockRuntime {
100 fn exists(&self, path: &Path) -> bool {
101 self.files.iter().any(|f| f == path)
102 }
103
104 async fn read_file(&self, _path: &Path) -> Result<Vec<u8>, RuntimeError> {
105 Err(RuntimeError::FileNotFound(PathBuf::new()))
106 }
107
108 async fn write_file(&self, _path: &Path, _content: &[u8]) -> Result<(), RuntimeError> {
109 Ok(())
110 }
111
112 async fn metadata(
113 &self,
114 path: &Path,
115 ) -> Result<crate::runtime::FileMetadata, RuntimeError> {
116 if self.files.iter().any(|f| f == path) {
117 Ok(crate::runtime::FileMetadata {
118 is_file: true,
119 is_dir: false,
120 size: 0,
121 modified: None,
122 })
123 } else {
124 Err(RuntimeError::FileNotFound(path.to_path_buf()))
125 }
126 }
127
128 fn resolve(&self, _specifier: &str, _from: &Path) -> Result<PathBuf, RuntimeError> {
129 Err(RuntimeError::FileNotFound(PathBuf::new()))
130 }
131
132 async fn create_dir(&self, _path: &Path, _recursive: bool) -> Result<(), RuntimeError> {
133 Ok(())
134 }
135
136 async fn remove_file(&self, _path: &Path) -> Result<(), RuntimeError> {
137 Ok(())
138 }
139
140 async fn read_dir(&self, _path: &Path) -> Result<Vec<String>, RuntimeError> {
141 Ok(vec![])
142 }
143
144 fn get_cwd(&self) -> Result<PathBuf, RuntimeError> {
145 Ok(PathBuf::from("/test"))
146 }
147 }
148
149 #[tokio::test]
150 async fn test_resolve_relative() {
151 let mut config = AnalyzerConfig::default();
152 config.cwd = Some(PathBuf::from("/test"));
153
154 let runtime = MockRuntime {
155 files: vec![PathBuf::from("/test/src/utils.ts")],
156 };
157
158 let resolver = ModuleResolver::new(config);
159 let from = PathBuf::from("/test/src/index.ts");
160
161 let result = resolver.resolve("./utils", &from, &runtime).await.unwrap();
162 assert!(matches!(result, ResolveResult::Local(_)));
163 }
164
165 #[tokio::test]
166 async fn test_resolve_external() {
167 let mut config = AnalyzerConfig::default();
168 config.external = vec!["react".to_string()];
169
170 let runtime = MockRuntime { files: vec![] };
171 let resolver = ModuleResolver::new(config);
172 let from = PathBuf::from("/test/src/index.ts");
173
174 let result = resolver.resolve("react", &from, &runtime).await.unwrap();
175 assert!(matches!(result, ResolveResult::External(_)));
176 }
177}