1use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use crate::cache::{self, CacheWriteHandle, ParseCache};
7use crate::error::Error;
8use crate::graph::ModuleGraph;
9use crate::lang::{self, LanguageSupport};
10use crate::vfs::{OsVfs, Vfs};
11use crate::walker;
12
13#[derive(Debug)]
15pub struct LoadedGraph {
16 pub graph: ModuleGraph,
17 pub root: PathBuf,
19 pub entry: PathBuf,
21 pub valid_extensions: &'static [&'static str],
23 pub from_cache: bool,
25 pub unresolvable_dynamic_count: usize,
27 pub unresolvable_dynamic_files: Vec<(PathBuf, usize)>,
29 pub file_warnings: Vec<String>,
31}
32
33#[must_use = "the CacheWriteHandle joins a background thread on drop"]
37pub fn load_graph(entry: &Path, no_cache: bool) -> Result<(LoadedGraph, CacheWriteHandle), Error> {
38 load_graph_with_vfs(entry, no_cache, Arc::new(OsVfs))
39}
40
41#[must_use = "the CacheWriteHandle joins a background thread on drop"]
49#[allow(clippy::needless_pass_by_value)] pub fn load_graph_with_vfs(
51 entry: &Path,
52 no_cache: bool,
53 vfs: Arc<dyn Vfs>,
54) -> Result<(LoadedGraph, CacheWriteHandle), Error> {
55 let entry = vfs
56 .canonicalize(entry)
57 .map_err(|e| Error::EntryNotFound(entry.to_path_buf(), e))?;
58
59 if vfs.is_dir(&entry) {
60 return Err(Error::EntryIsDirectory(entry));
61 }
62
63 let (root, kind) = lang::detect_project(&entry, &*vfs).ok_or_else(|| {
64 let ext = entry.extension().and_then(|e| e.to_str()).map(String::from);
65 Error::UnsupportedFileType(ext)
66 })?;
67
68 let lang_support: Box<dyn LanguageSupport> = match kind {
69 lang::ProjectKind::TypeScript => Box::new(lang::typescript::TypeScriptSupport::with_vfs(
70 &root,
71 vfs.clone(),
72 )),
73 lang::ProjectKind::Python => {
74 Box::new(lang::python::PythonSupport::with_vfs(&root, vfs.clone()))
75 }
76 };
77
78 let valid_extensions = lang_support.extensions();
79 let (result, handle) = build_or_load(&entry, &root, no_cache, lang_support.as_ref(), &*vfs);
80
81 Ok((
82 LoadedGraph {
83 graph: result.graph,
84 root,
85 entry,
86 valid_extensions,
87 from_cache: result.from_cache,
88 unresolvable_dynamic_count: result.unresolvable_dynamic_count,
89 unresolvable_dynamic_files: result.unresolvable_dynamic_files,
90 file_warnings: result.file_warnings,
91 },
92 handle,
93 ))
94}
95
96struct BuildResult {
101 graph: ModuleGraph,
102 unresolvable_dynamic_count: usize,
103 unresolvable_dynamic_files: Vec<(PathBuf, usize)>,
104 file_warnings: Vec<String>,
105 from_cache: bool,
106}
107
108fn build_or_load(
109 entry: &Path,
110 root: &Path,
111 no_cache: bool,
112 lang: &dyn LanguageSupport,
113 vfs: &dyn Vfs,
114) -> (BuildResult, CacheWriteHandle) {
115 let mut cache = if no_cache {
116 ParseCache::new()
117 } else {
118 ParseCache::load(root)
119 };
120
121 if !no_cache {
123 let resolve_fn = |spec: &str| lang.resolve(root, spec).is_some();
124 match cache.try_load_graph(entry, &resolve_fn) {
125 cache::GraphCacheResult::Hit {
126 graph,
127 unresolvable_dynamic,
128 unresolvable_dynamic_files,
129 unresolved_specifiers,
130 needs_resave,
131 } => {
132 let handle = if needs_resave {
133 cache.save(
134 root,
135 entry,
136 &graph,
137 unresolved_specifiers,
138 unresolvable_dynamic,
139 unresolvable_dynamic_files.clone(),
140 )
141 } else {
142 CacheWriteHandle::none()
143 };
144 return (
145 BuildResult {
146 graph,
147 unresolvable_dynamic_count: unresolvable_dynamic,
148 unresolvable_dynamic_files,
149 file_warnings: Vec::new(),
150 from_cache: true,
151 },
152 handle,
153 );
154 }
155 cache::GraphCacheResult::Stale {
156 mut graph,
157 unresolvable_dynamic,
158 unresolvable_dynamic_files,
159 changed_files,
160 } => {
161 if let Some(result) = try_incremental_update(
164 &mut cache,
165 &mut graph,
166 &changed_files,
167 unresolvable_dynamic,
168 unresolvable_dynamic_files,
169 lang,
170 vfs,
171 ) {
172 graph.compute_package_info();
173 let handle = cache.save_incremental(
174 root,
175 entry,
176 &graph,
177 &changed_files,
178 result.unresolvable_dynamic,
179 result.unresolvable_dynamic_files.clone(),
180 );
181 return (
182 BuildResult {
183 graph,
184 unresolvable_dynamic_count: result.unresolvable_dynamic,
185 unresolvable_dynamic_files: result.unresolvable_dynamic_files,
186 file_warnings: Vec::new(),
187 from_cache: true,
188 },
189 handle,
190 );
191 }
192 }
194 cache::GraphCacheResult::Miss => {}
195 }
196 }
197
198 let result = walker::build_graph(entry, root, lang, &mut cache, vfs);
200 let unresolvable_count: usize = result.unresolvable_dynamic.iter().map(|(_, c)| c).sum();
201 let handle = cache.save(
202 root,
203 entry,
204 &result.graph,
205 result.unresolved_specifiers,
206 unresolvable_count,
207 result.unresolvable_dynamic.clone(),
208 );
209 (
210 BuildResult {
211 graph: result.graph,
212 unresolvable_dynamic_count: unresolvable_count,
213 unresolvable_dynamic_files: result.unresolvable_dynamic,
214 file_warnings: result.file_warnings,
215 from_cache: false,
216 },
217 handle,
218 )
219}
220
221struct IncrementalResult {
222 unresolvable_dynamic: usize,
223 unresolvable_dynamic_files: Vec<(PathBuf, usize)>,
224}
225
226#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
230fn try_incremental_update(
231 cache: &mut ParseCache,
232 graph: &mut ModuleGraph,
233 changed_files: &[PathBuf],
234 old_unresolvable_total: usize,
235 mut unresolvable_files: Vec<(PathBuf, usize)>,
236 lang: &dyn LanguageSupport,
237 vfs: &dyn Vfs,
238) -> Option<IncrementalResult> {
239 let mut unresolvable_delta: isize = 0;
240
241 for path in changed_files {
242 let old_result = cache.lookup_unchecked(path)?;
244 let old_import_count = old_result.imports.len();
245 let old_unresolvable = old_result.unresolvable_dynamic;
246 let old_imports: Vec<_> = old_result
247 .imports
248 .iter()
249 .map(|i| (i.specifier.as_str(), i.kind))
250 .collect();
251
252 let source = vfs.read_to_string(path).ok()?;
254 let new_result = lang.parse(path, &source).ok()?;
255
256 if new_result.imports.len() != old_import_count
258 || new_result.imports.iter().zip(old_imports.iter()).any(
259 |(new, &(old_spec, old_kind))| new.specifier != old_spec || new.kind != old_kind,
260 )
261 {
262 return None;
263 }
264
265 unresolvable_delta += new_result.unresolvable_dynamic as isize - old_unresolvable as isize;
267
268 unresolvable_files.retain(|(p, _)| p != path);
270 if new_result.unresolvable_dynamic > 0 {
271 unresolvable_files.push((path.clone(), new_result.unresolvable_dynamic));
272 }
273
274 let mid = *graph.path_to_id.get(path)?;
276 let new_size = source.len() as u64;
277 graph.modules[mid.0 as usize].size_bytes = new_size;
278
279 #[allow(clippy::or_fun_call)]
281 let dir = path.parent().unwrap_or(Path::new("."));
282 let resolved_paths: Vec<Option<PathBuf>> = new_result
283 .imports
284 .iter()
285 .map(|imp| lang.resolve(dir, &imp.specifier))
286 .collect();
287 if let Ok(meta) = vfs.metadata(path)
288 && let Some(mtime) = meta.mtime_nanos
289 {
290 cache.insert(path.clone(), new_size, mtime, new_result, resolved_paths);
291 }
292 }
293
294 let new_total = (old_unresolvable_total as isize + unresolvable_delta).max(0) as usize;
295 debug_assert_eq!(
296 new_total,
297 unresolvable_files.iter().map(|(_, c)| c).sum::<usize>(),
298 "unresolvable_dynamic total drifted from per-file sum"
299 );
300 Some(IncrementalResult {
301 unresolvable_dynamic: new_total,
302 unresolvable_dynamic_files: unresolvable_files,
303 })
304}