1use std::fs;
4use std::path::{Path, PathBuf};
5use std::time::SystemTime;
6
7use crate::cache::{self, CacheWriteHandle, ParseCache};
8use crate::error::Error;
9use crate::graph::ModuleGraph;
10use crate::lang::{self, LanguageSupport};
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"]
41pub fn load_graph(entry: &Path, no_cache: bool) -> Result<(LoadedGraph, CacheWriteHandle), Error> {
42 let entry = entry
43 .canonicalize()
44 .map_err(|e| Error::EntryNotFound(entry.to_path_buf(), e))?;
45
46 if entry.is_dir() {
47 return Err(Error::EntryIsDirectory(entry));
48 }
49
50 let (root, kind) = lang::detect_project(&entry).ok_or_else(|| {
51 let ext = entry
52 .extension()
53 .and_then(|e| e.to_str())
54 .unwrap_or("(none)");
55 Error::UnsupportedFileType(ext.to_string())
56 })?;
57
58 let lang_support: Box<dyn LanguageSupport> = match kind {
59 lang::ProjectKind::TypeScript => {
60 Box::new(lang::typescript::TypeScriptSupport::new(&root))
61 }
62 lang::ProjectKind::Python => Box::new(lang::python::PythonSupport::new(&root)),
63 };
64
65 let valid_extensions = lang_support.extensions();
66 let (result, handle) = build_or_load(&entry, &root, no_cache, lang_support.as_ref());
67
68 Ok((
69 LoadedGraph {
70 graph: result.graph,
71 root,
72 entry,
73 valid_extensions,
74 from_cache: result.from_cache,
75 unresolvable_dynamic_count: result.unresolvable_dynamic_count,
76 unresolvable_dynamic_files: result.unresolvable_dynamic_files,
77 file_warnings: result.file_warnings,
78 },
79 handle,
80 ))
81}
82
83struct BuildResult {
88 graph: ModuleGraph,
89 unresolvable_dynamic_count: usize,
90 unresolvable_dynamic_files: Vec<(PathBuf, usize)>,
91 file_warnings: Vec<String>,
92 from_cache: bool,
93}
94
95fn build_or_load(
96 entry: &Path,
97 root: &Path,
98 no_cache: bool,
99 lang: &dyn LanguageSupport,
100) -> (BuildResult, CacheWriteHandle) {
101 let mut cache = if no_cache {
102 ParseCache::new()
103 } else {
104 ParseCache::load(root)
105 };
106
107 if !no_cache {
109 let resolve_fn = |spec: &str| lang.resolve(root, spec).is_some();
110 match cache.try_load_graph(entry, &resolve_fn) {
111 cache::GraphCacheResult::Hit {
112 graph,
113 unresolvable_dynamic,
114 unresolved_specifiers,
115 needs_resave,
116 } => {
117 let handle = if needs_resave {
118 cache.save(root, entry, &graph, unresolved_specifiers, unresolvable_dynamic)
119 } else {
120 CacheWriteHandle::none()
121 };
122 return (
123 BuildResult {
124 graph,
125 unresolvable_dynamic_count: unresolvable_dynamic,
126 unresolvable_dynamic_files: Vec::new(),
127 file_warnings: Vec::new(),
128 from_cache: true,
129 },
130 handle,
131 );
132 }
133 cache::GraphCacheResult::Stale {
134 mut graph,
135 unresolvable_dynamic,
136 changed_files,
137 } => {
138 if let Some(result) = try_incremental_update(
141 &mut cache,
142 &mut graph,
143 &changed_files,
144 unresolvable_dynamic,
145 lang,
146 ) {
147 graph.compute_package_info();
148 let handle = cache.save_incremental(
149 root,
150 entry,
151 &graph,
152 &changed_files,
153 result.unresolvable_dynamic,
154 );
155 return (
156 BuildResult {
157 graph,
158 unresolvable_dynamic_count: result.unresolvable_dynamic,
159 unresolvable_dynamic_files: Vec::new(),
160 file_warnings: Vec::new(),
161 from_cache: true,
162 },
163 handle,
164 );
165 }
166 }
168 cache::GraphCacheResult::Miss => {}
169 }
170 }
171
172 let result = walker::build_graph(entry, root, lang, &mut cache);
174 let unresolvable_count: usize = result.unresolvable_dynamic.iter().map(|(_, c)| c).sum();
175 let handle = cache.save(
176 root,
177 entry,
178 &result.graph,
179 result.unresolved_specifiers,
180 unresolvable_count,
181 );
182 (
183 BuildResult {
184 graph: result.graph,
185 unresolvable_dynamic_count: unresolvable_count,
186 unresolvable_dynamic_files: result.unresolvable_dynamic,
187 file_warnings: result.file_warnings,
188 from_cache: false,
189 },
190 handle,
191 )
192}
193
194struct IncrementalResult {
195 unresolvable_dynamic: usize,
196}
197
198#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
202fn try_incremental_update(
203 cache: &mut ParseCache,
204 graph: &mut ModuleGraph,
205 changed_files: &[PathBuf],
206 old_unresolvable_total: usize,
207 lang: &dyn LanguageSupport,
208) -> Option<IncrementalResult> {
209 let mut unresolvable_delta: isize = 0;
210
211 for path in changed_files {
212 let old_result = cache.lookup_unchecked(path)?;
214 let old_import_count = old_result.imports.len();
215 let old_unresolvable = old_result.unresolvable_dynamic;
216 let old_imports: Vec<_> = old_result
217 .imports
218 .iter()
219 .map(|i| (i.specifier.as_str(), i.kind))
220 .collect();
221
222 let source = std::fs::read_to_string(path).ok()?;
224 let new_result = lang.parse(path, &source).ok()?;
225
226 if new_result.imports.len() != old_import_count
228 || new_result
229 .imports
230 .iter()
231 .zip(old_imports.iter())
232 .any(|(new, &(old_spec, old_kind))| {
233 new.specifier != old_spec || new.kind != old_kind
234 })
235 {
236 return None;
237 }
238
239 unresolvable_delta +=
241 new_result.unresolvable_dynamic as isize - old_unresolvable as isize;
242
243 let mid = *graph.path_to_id.get(path)?;
245 let new_size = source.len() as u64;
246 graph.modules[mid.0 as usize].size_bytes = new_size;
247
248 #[allow(clippy::or_fun_call)]
250 let dir = path.parent().unwrap_or(Path::new("."));
251 let resolved_paths: Vec<Option<PathBuf>> = new_result
252 .imports
253 .iter()
254 .map(|imp| lang.resolve(dir, &imp.specifier))
255 .collect();
256 if let Ok(meta) = fs::metadata(path) {
257 let mtime = meta.modified().ok()
258 .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
259 .map(|d: std::time::Duration| d.as_nanos());
260 if let Some(mtime) = mtime {
261 cache.insert(path.clone(), new_size, mtime, new_result, resolved_paths);
262 }
263 }
264 }
265
266 let new_total = (old_unresolvable_total as isize + unresolvable_delta).max(0) as usize;
267 Some(IncrementalResult {
268 unresolvable_dynamic: new_total,
269 })
270}