1use std::collections::HashMap;
2use std::path::Path;
3
4use bincode::{Decode, Encode};
5
6use oxc_span::Span;
7
8use crate::extract::{ExportName, MemberAccess, MemberKind};
9
10const CACHE_VERSION: u32 = 5;
12
13const MAX_CACHE_SIZE: usize = 256 * 1024 * 1024;
15
16#[derive(Debug, Encode, Decode)]
18pub struct CacheStore {
19 version: u32,
20 entries: HashMap<String, CachedModule>,
22}
23
24#[derive(Debug, Clone, Encode, Decode)]
26pub struct CachedModule {
27 pub content_hash: u64,
29 pub exports: Vec<CachedExport>,
31 pub imports: Vec<CachedImport>,
33 pub re_exports: Vec<CachedReExport>,
35 pub dynamic_imports: Vec<CachedDynamicImport>,
37 pub require_calls: Vec<CachedRequireCall>,
39 pub member_accesses: Vec<MemberAccess>,
41 pub whole_object_uses: Vec<String>,
43 pub dynamic_import_patterns: Vec<CachedDynamicImportPattern>,
45 pub has_cjs_exports: bool,
47}
48
49#[derive(Debug, Clone, Encode, Decode)]
50pub struct CachedExport {
51 pub name: String,
52 pub is_default: bool,
53 pub is_type_only: bool,
54 pub local_name: Option<String>,
55 pub span_start: u32,
56 pub span_end: u32,
57 pub members: Vec<CachedMember>,
58}
59
60const IMPORT_KIND_NAMED: u8 = 0;
63const IMPORT_KIND_DEFAULT: u8 = 1;
64const IMPORT_KIND_NAMESPACE: u8 = 2;
65const IMPORT_KIND_SIDE_EFFECT: u8 = 3;
66
67#[derive(Debug, Clone, Encode, Decode)]
68pub struct CachedImport {
69 pub source: String,
70 pub imported_name: String,
72 pub local_name: String,
73 pub is_type_only: bool,
74 pub kind: u8,
76 pub span_start: u32,
77 pub span_end: u32,
78}
79
80#[derive(Debug, Clone, Encode, Decode)]
81pub struct CachedDynamicImport {
82 pub source: String,
83 pub span_start: u32,
84 pub span_end: u32,
85}
86
87#[derive(Debug, Clone, Encode, Decode)]
88pub struct CachedRequireCall {
89 pub source: String,
90 pub span_start: u32,
91 pub span_end: u32,
92 pub destructured_names: Vec<String>,
93 pub local_name: Option<String>,
94}
95
96#[derive(Debug, Clone, Encode, Decode)]
97pub struct CachedReExport {
98 pub source: String,
99 pub imported_name: String,
100 pub exported_name: String,
101 pub is_type_only: bool,
102}
103
104#[derive(Debug, Clone, Encode, Decode)]
105pub struct CachedMember {
106 pub name: String,
107 pub kind: String,
108 pub span_start: u32,
109 pub span_end: u32,
110}
111
112#[derive(Debug, Clone, Encode, Decode)]
113pub struct CachedDynamicImportPattern {
114 pub prefix: String,
115 pub suffix: Option<String>,
116 pub span_start: u32,
117 pub span_end: u32,
118}
119
120impl CacheStore {
121 pub fn new() -> Self {
123 Self {
124 version: CACHE_VERSION,
125 entries: HashMap::new(),
126 }
127 }
128
129 pub fn load(cache_dir: &Path) -> Option<Self> {
131 let cache_file = cache_dir.join("cache.bin");
132 let data = std::fs::read(&cache_file).ok()?;
133 if data.len() > MAX_CACHE_SIZE {
134 tracing::warn!(
135 size_mb = data.len() / (1024 * 1024),
136 "Cache file exceeds size limit, ignoring"
137 );
138 return None;
139 }
140 let (store, _): (Self, usize) =
141 bincode::decode_from_slice(&data, bincode::config::standard()).ok()?;
142 if store.version != CACHE_VERSION {
143 return None;
144 }
145 Some(store)
146 }
147
148 pub fn save(&self, cache_dir: &Path) -> Result<(), String> {
150 std::fs::create_dir_all(cache_dir)
151 .map_err(|e| format!("Failed to create cache dir: {e}"))?;
152 let cache_file = cache_dir.join("cache.bin");
153 let data = bincode::encode_to_vec(self, bincode::config::standard())
154 .map_err(|e| format!("Failed to serialize cache: {e}"))?;
155 std::fs::write(&cache_file, data).map_err(|e| format!("Failed to write cache: {e}"))?;
156 Ok(())
157 }
158
159 pub fn get(&self, path: &Path, content_hash: u64) -> Option<&CachedModule> {
162 let key = path.to_string_lossy().to_string();
163 let entry = self.entries.get(&key)?;
164 if entry.content_hash == content_hash {
165 Some(entry)
166 } else {
167 None
168 }
169 }
170
171 pub fn insert(&mut self, path: &Path, module: CachedModule) {
173 let key = path.to_string_lossy().to_string();
174 self.entries.insert(key, module);
175 }
176
177 pub fn len(&self) -> usize {
179 self.entries.len()
180 }
181
182 pub fn is_empty(&self) -> bool {
184 self.entries.is_empty()
185 }
186}
187
188impl Default for CacheStore {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194pub fn cached_to_module(
196 cached: &CachedModule,
197 file_id: crate::discover::FileId,
198) -> crate::extract::ModuleInfo {
199 use crate::extract::*;
200
201 let exports = cached
202 .exports
203 .iter()
204 .map(|e| ExportInfo {
205 name: if e.is_default {
206 ExportName::Default
207 } else {
208 ExportName::Named(e.name.clone())
209 },
210 local_name: e.local_name.clone(),
211 is_type_only: e.is_type_only,
212 span: Span::new(e.span_start, e.span_end),
213 members: e
214 .members
215 .iter()
216 .map(|m| MemberInfo {
217 name: m.name.clone(),
218 kind: match m.kind.as_str() {
219 "enum" => MemberKind::EnumMember,
220 "method" => MemberKind::ClassMethod,
221 "property" => MemberKind::ClassProperty,
222 other => {
223 tracing::warn!(
224 kind = other,
225 "Unknown cached member kind, defaulting to ClassProperty"
226 );
227 MemberKind::ClassProperty
228 }
229 },
230 span: Span::new(m.span_start, m.span_end),
231 })
232 .collect(),
233 })
234 .collect();
235
236 let imports = cached
237 .imports
238 .iter()
239 .map(|i| ImportInfo {
240 source: i.source.clone(),
241 imported_name: match i.kind {
242 IMPORT_KIND_DEFAULT => ImportedName::Default,
243 IMPORT_KIND_NAMESPACE => ImportedName::Namespace,
244 IMPORT_KIND_SIDE_EFFECT => ImportedName::SideEffect,
245 _ => ImportedName::Named(i.imported_name.clone()),
247 },
248 local_name: i.local_name.clone(),
249 is_type_only: i.is_type_only,
250 span: Span::new(i.span_start, i.span_end),
251 })
252 .collect();
253
254 let re_exports = cached
255 .re_exports
256 .iter()
257 .map(|r| ReExportInfo {
258 source: r.source.clone(),
259 imported_name: r.imported_name.clone(),
260 exported_name: r.exported_name.clone(),
261 is_type_only: r.is_type_only,
262 })
263 .collect();
264
265 let dynamic_imports = cached
266 .dynamic_imports
267 .iter()
268 .map(|d| DynamicImportInfo {
269 source: d.source.clone(),
270 span: Span::new(d.span_start, d.span_end),
271 })
272 .collect();
273
274 let require_calls = cached
275 .require_calls
276 .iter()
277 .map(|r| RequireCallInfo {
278 source: r.source.clone(),
279 span: Span::new(r.span_start, r.span_end),
280 destructured_names: r.destructured_names.clone(),
281 local_name: r.local_name.clone(),
282 })
283 .collect();
284
285 let dynamic_import_patterns = cached
286 .dynamic_import_patterns
287 .iter()
288 .map(|p| crate::extract::DynamicImportPattern {
289 prefix: p.prefix.clone(),
290 suffix: p.suffix.clone(),
291 span: Span::new(p.span_start, p.span_end),
292 })
293 .collect();
294
295 ModuleInfo {
296 file_id,
297 exports,
298 imports,
299 re_exports,
300 dynamic_imports,
301 dynamic_import_patterns,
302 require_calls,
303 member_accesses: cached.member_accesses.clone(),
304 whole_object_uses: cached.whole_object_uses.clone(),
305 has_cjs_exports: cached.has_cjs_exports,
306 content_hash: cached.content_hash,
307 }
308}
309
310pub fn module_to_cached(module: &crate::extract::ModuleInfo) -> CachedModule {
312 CachedModule {
313 content_hash: module.content_hash,
314 exports: module
315 .exports
316 .iter()
317 .map(|e| CachedExport {
318 name: match &e.name {
319 ExportName::Named(n) => n.clone(),
320 ExportName::Default => "default".to_string(),
321 },
322 is_default: matches!(e.name, ExportName::Default),
323 is_type_only: e.is_type_only,
324 local_name: e.local_name.clone(),
325 span_start: e.span.start,
326 span_end: e.span.end,
327 members: e
328 .members
329 .iter()
330 .map(|m| CachedMember {
331 name: m.name.clone(),
332 kind: match m.kind {
333 MemberKind::EnumMember => "enum".to_string(),
334 MemberKind::ClassMethod => "method".to_string(),
335 MemberKind::ClassProperty => "property".to_string(),
336 },
337 span_start: m.span.start,
338 span_end: m.span.end,
339 })
340 .collect(),
341 })
342 .collect(),
343 imports: module
344 .imports
345 .iter()
346 .map(|i| {
347 let (kind, imported_name) = match &i.imported_name {
348 crate::extract::ImportedName::Named(n) => (IMPORT_KIND_NAMED, n.clone()),
349 crate::extract::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
350 crate::extract::ImportedName::Namespace => {
351 (IMPORT_KIND_NAMESPACE, String::new())
352 }
353 crate::extract::ImportedName::SideEffect => {
354 (IMPORT_KIND_SIDE_EFFECT, String::new())
355 }
356 };
357 CachedImport {
358 source: i.source.clone(),
359 imported_name,
360 local_name: i.local_name.clone(),
361 is_type_only: i.is_type_only,
362 kind,
363 span_start: i.span.start,
364 span_end: i.span.end,
365 }
366 })
367 .collect(),
368 re_exports: module
369 .re_exports
370 .iter()
371 .map(|r| CachedReExport {
372 source: r.source.clone(),
373 imported_name: r.imported_name.clone(),
374 exported_name: r.exported_name.clone(),
375 is_type_only: r.is_type_only,
376 })
377 .collect(),
378 dynamic_imports: module
379 .dynamic_imports
380 .iter()
381 .map(|d| CachedDynamicImport {
382 source: d.source.clone(),
383 span_start: d.span.start,
384 span_end: d.span.end,
385 })
386 .collect(),
387 require_calls: module
388 .require_calls
389 .iter()
390 .map(|r| CachedRequireCall {
391 source: r.source.clone(),
392 span_start: r.span.start,
393 span_end: r.span.end,
394 destructured_names: r.destructured_names.clone(),
395 local_name: r.local_name.clone(),
396 })
397 .collect(),
398 member_accesses: module.member_accesses.clone(),
399 whole_object_uses: module.whole_object_uses.clone(),
400 dynamic_import_patterns: module
401 .dynamic_import_patterns
402 .iter()
403 .map(|p| CachedDynamicImportPattern {
404 prefix: p.prefix.clone(),
405 suffix: p.suffix.clone(),
406 span_start: p.span.start,
407 span_end: p.span.end,
408 })
409 .collect(),
410 has_cjs_exports: module.has_cjs_exports,
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use crate::discover::FileId;
418 use crate::extract::*;
419
420 #[test]
421 fn cache_store_new_is_empty() {
422 let store = CacheStore::new();
423 assert!(store.is_empty());
424 assert_eq!(store.len(), 0);
425 }
426
427 #[test]
428 fn cache_store_default_is_empty() {
429 let store = CacheStore::default();
430 assert!(store.is_empty());
431 }
432
433 #[test]
434 fn cache_store_insert_and_get() {
435 let mut store = CacheStore::new();
436 let module = CachedModule {
437 content_hash: 42,
438 exports: vec![],
439 imports: vec![],
440 re_exports: vec![],
441 dynamic_imports: vec![],
442 require_calls: vec![],
443 member_accesses: vec![],
444 whole_object_uses: vec![],
445 dynamic_import_patterns: vec![],
446 has_cjs_exports: false,
447 };
448 store.insert(Path::new("test.ts"), module);
449 assert_eq!(store.len(), 1);
450 assert!(!store.is_empty());
451 assert!(store.get(Path::new("test.ts"), 42).is_some());
452 }
453
454 #[test]
455 fn cache_store_hash_mismatch_returns_none() {
456 let mut store = CacheStore::new();
457 let module = CachedModule {
458 content_hash: 42,
459 exports: vec![],
460 imports: vec![],
461 re_exports: vec![],
462 dynamic_imports: vec![],
463 require_calls: vec![],
464 member_accesses: vec![],
465 whole_object_uses: vec![],
466 dynamic_import_patterns: vec![],
467 has_cjs_exports: false,
468 };
469 store.insert(Path::new("test.ts"), module);
470 assert!(store.get(Path::new("test.ts"), 99).is_none());
471 }
472
473 #[test]
474 fn cache_store_missing_key_returns_none() {
475 let store = CacheStore::new();
476 assert!(store.get(Path::new("nonexistent.ts"), 42).is_none());
477 }
478
479 #[test]
480 fn cache_store_overwrite_entry() {
481 let mut store = CacheStore::new();
482 let m1 = CachedModule {
483 content_hash: 1,
484 exports: vec![],
485 imports: vec![],
486 re_exports: vec![],
487 dynamic_imports: vec![],
488 require_calls: vec![],
489 member_accesses: vec![],
490 whole_object_uses: vec![],
491 dynamic_import_patterns: vec![],
492 has_cjs_exports: false,
493 };
494 let m2 = CachedModule {
495 content_hash: 2,
496 exports: vec![],
497 imports: vec![],
498 re_exports: vec![],
499 dynamic_imports: vec![],
500 require_calls: vec![],
501 member_accesses: vec![],
502 whole_object_uses: vec![],
503 dynamic_import_patterns: vec![],
504 has_cjs_exports: false,
505 };
506 store.insert(Path::new("test.ts"), m1);
507 store.insert(Path::new("test.ts"), m2);
508 assert_eq!(store.len(), 1);
509 assert!(store.get(Path::new("test.ts"), 1).is_none());
510 assert!(store.get(Path::new("test.ts"), 2).is_some());
511 }
512
513 #[test]
514 fn module_to_cached_roundtrip_named_export() {
515 let module = ModuleInfo {
516 file_id: FileId(0),
517 exports: vec![ExportInfo {
518 name: ExportName::Named("foo".to_string()),
519 local_name: Some("foo".to_string()),
520 is_type_only: false,
521 span: Span::new(10, 20),
522 members: vec![],
523 }],
524 imports: vec![],
525 re_exports: vec![],
526 dynamic_imports: vec![],
527 require_calls: vec![],
528 member_accesses: vec![],
529 whole_object_uses: vec![],
530 dynamic_import_patterns: vec![],
531 has_cjs_exports: false,
532 content_hash: 123,
533 };
534
535 let cached = module_to_cached(&module);
536 let restored = cached_to_module(&cached, FileId(0));
537
538 assert_eq!(restored.exports.len(), 1);
539 assert_eq!(
540 restored.exports[0].name,
541 ExportName::Named("foo".to_string())
542 );
543 assert!(!restored.exports[0].is_type_only);
544 assert_eq!(restored.exports[0].span.start, 10);
545 assert_eq!(restored.exports[0].span.end, 20);
546 assert_eq!(restored.content_hash, 123);
547 }
548
549 #[test]
550 fn module_to_cached_roundtrip_default_export() {
551 let module = ModuleInfo {
552 file_id: FileId(0),
553 exports: vec![ExportInfo {
554 name: ExportName::Default,
555 local_name: None,
556 is_type_only: false,
557 span: Span::new(0, 10),
558 members: vec![],
559 }],
560 imports: vec![],
561 re_exports: vec![],
562 dynamic_imports: vec![],
563 require_calls: vec![],
564 member_accesses: vec![],
565 whole_object_uses: vec![],
566 dynamic_import_patterns: vec![],
567 has_cjs_exports: false,
568 content_hash: 456,
569 };
570
571 let cached = module_to_cached(&module);
572 let restored = cached_to_module(&cached, FileId(0));
573
574 assert_eq!(restored.exports[0].name, ExportName::Default);
575 }
576
577 #[test]
578 fn module_to_cached_roundtrip_imports() {
579 let module = ModuleInfo {
580 file_id: FileId(0),
581 exports: vec![],
582 imports: vec![
583 ImportInfo {
584 source: "./utils".to_string(),
585 imported_name: ImportedName::Named("foo".to_string()),
586 local_name: "foo".to_string(),
587 is_type_only: false,
588 span: Span::new(0, 10),
589 },
590 ImportInfo {
591 source: "react".to_string(),
592 imported_name: ImportedName::Default,
593 local_name: "React".to_string(),
594 is_type_only: false,
595 span: Span::new(15, 30),
596 },
597 ImportInfo {
598 source: "./all".to_string(),
599 imported_name: ImportedName::Namespace,
600 local_name: "all".to_string(),
601 is_type_only: false,
602 span: Span::new(35, 50),
603 },
604 ImportInfo {
605 source: "./styles.css".to_string(),
606 imported_name: ImportedName::SideEffect,
607 local_name: String::new(),
608 is_type_only: false,
609 span: Span::new(55, 70),
610 },
611 ],
612 re_exports: vec![],
613 dynamic_imports: vec![],
614 require_calls: vec![],
615 member_accesses: vec![],
616 whole_object_uses: vec![],
617 dynamic_import_patterns: vec![],
618 has_cjs_exports: false,
619 content_hash: 789,
620 };
621
622 let cached = module_to_cached(&module);
623 let restored = cached_to_module(&cached, FileId(0));
624
625 assert_eq!(restored.imports.len(), 4);
626 assert_eq!(
627 restored.imports[0].imported_name,
628 ImportedName::Named("foo".to_string())
629 );
630 assert_eq!(restored.imports[0].span.start, 0);
631 assert_eq!(restored.imports[0].span.end, 10);
632 assert_eq!(restored.imports[1].imported_name, ImportedName::Default);
633 assert_eq!(restored.imports[1].span.start, 15);
634 assert_eq!(restored.imports[1].span.end, 30);
635 assert_eq!(restored.imports[2].imported_name, ImportedName::Namespace);
636 assert_eq!(restored.imports[2].span.start, 35);
637 assert_eq!(restored.imports[2].span.end, 50);
638 assert_eq!(restored.imports[3].imported_name, ImportedName::SideEffect);
639 assert_eq!(restored.imports[3].span.start, 55);
640 assert_eq!(restored.imports[3].span.end, 70);
641 }
642
643 #[test]
644 fn module_to_cached_roundtrip_re_exports() {
645 let module = ModuleInfo {
646 file_id: FileId(0),
647 exports: vec![],
648 imports: vec![],
649 re_exports: vec![ReExportInfo {
650 source: "./module".to_string(),
651 imported_name: "foo".to_string(),
652 exported_name: "bar".to_string(),
653 is_type_only: true,
654 }],
655 dynamic_imports: vec![],
656 require_calls: vec![],
657 member_accesses: vec![],
658 whole_object_uses: vec![],
659 dynamic_import_patterns: vec![],
660 has_cjs_exports: false,
661 content_hash: 0,
662 };
663
664 let cached = module_to_cached(&module);
665 let restored = cached_to_module(&cached, FileId(0));
666
667 assert_eq!(restored.re_exports.len(), 1);
668 assert_eq!(restored.re_exports[0].source, "./module");
669 assert_eq!(restored.re_exports[0].imported_name, "foo");
670 assert_eq!(restored.re_exports[0].exported_name, "bar");
671 assert!(restored.re_exports[0].is_type_only);
672 }
673
674 #[test]
675 fn module_to_cached_roundtrip_dynamic_imports() {
676 let module = ModuleInfo {
677 file_id: FileId(0),
678 exports: vec![],
679 imports: vec![],
680 re_exports: vec![],
681 dynamic_imports: vec![DynamicImportInfo {
682 source: "./lazy".to_string(),
683 span: Span::new(0, 10),
684 }],
685 require_calls: vec![RequireCallInfo {
686 source: "fs".to_string(),
687 span: Span::new(15, 25),
688 destructured_names: Vec::new(),
689 local_name: None,
690 }],
691 member_accesses: vec![MemberAccess {
692 object: "Status".to_string(),
693 member: "Active".to_string(),
694 }],
695 whole_object_uses: vec![],
696 dynamic_import_patterns: vec![],
697 has_cjs_exports: true,
698 content_hash: 0,
699 };
700
701 let cached = module_to_cached(&module);
702 let restored = cached_to_module(&cached, FileId(0));
703
704 assert_eq!(restored.dynamic_imports.len(), 1);
705 assert_eq!(restored.dynamic_imports[0].source, "./lazy");
706 assert_eq!(restored.dynamic_imports[0].span.start, 0);
707 assert_eq!(restored.dynamic_imports[0].span.end, 10);
708 assert_eq!(restored.require_calls.len(), 1);
709 assert_eq!(restored.require_calls[0].source, "fs");
710 assert_eq!(restored.require_calls[0].span.start, 15);
711 assert_eq!(restored.require_calls[0].span.end, 25);
712 assert_eq!(restored.member_accesses.len(), 1);
713 assert_eq!(restored.member_accesses[0].object, "Status");
714 assert_eq!(restored.member_accesses[0].member, "Active");
715 assert!(restored.has_cjs_exports);
716 }
717
718 #[test]
719 fn module_to_cached_roundtrip_members() {
720 let module = ModuleInfo {
721 file_id: FileId(0),
722 exports: vec![ExportInfo {
723 name: ExportName::Named("Color".to_string()),
724 local_name: Some("Color".to_string()),
725 is_type_only: false,
726 span: Span::new(0, 50),
727 members: vec![
728 MemberInfo {
729 name: "Red".to_string(),
730 kind: MemberKind::EnumMember,
731 span: Span::new(10, 15),
732 },
733 MemberInfo {
734 name: "greet".to_string(),
735 kind: MemberKind::ClassMethod,
736 span: Span::new(20, 30),
737 },
738 MemberInfo {
739 name: "name".to_string(),
740 kind: MemberKind::ClassProperty,
741 span: Span::new(35, 45),
742 },
743 ],
744 }],
745 imports: vec![],
746 re_exports: vec![],
747 dynamic_imports: vec![],
748 require_calls: vec![],
749 member_accesses: vec![],
750 whole_object_uses: vec![],
751 dynamic_import_patterns: vec![],
752 has_cjs_exports: false,
753 content_hash: 0,
754 };
755
756 let cached = module_to_cached(&module);
757 let restored = cached_to_module(&cached, FileId(0));
758
759 assert_eq!(restored.exports[0].members.len(), 3);
760 assert_eq!(restored.exports[0].members[0].kind, MemberKind::EnumMember);
761 assert_eq!(restored.exports[0].members[1].kind, MemberKind::ClassMethod);
762 assert_eq!(
763 restored.exports[0].members[2].kind,
764 MemberKind::ClassProperty
765 );
766 }
767
768 #[test]
769 fn cache_load_nonexistent_returns_none() {
770 let result = CacheStore::load(Path::new("/nonexistent/path"));
771 assert!(result.is_none());
772 }
773
774 fn test_cache_dir(name: &str) -> std::path::PathBuf {
776 let dir = std::env::temp_dir()
777 .join("fallow_cache_tests")
778 .join(name)
779 .join(format!("{}", std::process::id()));
780 let _ = std::fs::remove_dir_all(&dir);
782 std::fs::create_dir_all(&dir).unwrap();
783 dir
784 }
785
786 #[test]
787 fn cache_save_and_load_roundtrip() {
788 let dir = test_cache_dir("roundtrip");
789 let mut store = CacheStore::new();
790 let module = CachedModule {
791 content_hash: 42,
792 exports: vec![],
793 imports: vec![],
794 re_exports: vec![],
795 dynamic_imports: vec![],
796 require_calls: vec![],
797 member_accesses: vec![],
798 whole_object_uses: vec![],
799 dynamic_import_patterns: vec![],
800 has_cjs_exports: false,
801 };
802 store.insert(Path::new("test.ts"), module);
803 store.save(&dir).unwrap();
804
805 let loaded = CacheStore::load(&dir);
806 assert!(loaded.is_some());
807 let loaded = loaded.unwrap();
808 assert_eq!(loaded.len(), 1);
809 assert!(loaded.get(Path::new("test.ts"), 42).is_some());
810
811 let _ = std::fs::remove_dir_all(&dir);
812 }
813
814 #[test]
815 fn cache_version_mismatch_returns_none() {
816 let dir = test_cache_dir("version_mismatch");
817 let mut store = CacheStore::new();
818 let module = CachedModule {
819 content_hash: 42,
820 exports: vec![],
821 imports: vec![],
822 re_exports: vec![],
823 dynamic_imports: vec![],
824 require_calls: vec![],
825 member_accesses: vec![],
826 whole_object_uses: vec![],
827 dynamic_import_patterns: vec![],
828 has_cjs_exports: false,
829 };
830 store.insert(Path::new("test.ts"), module);
831 store.save(&dir).unwrap();
832
833 assert!(CacheStore::load(&dir).is_some());
835
836 let cache_file = dir.join("cache.bin");
842 let mut data = std::fs::read(&cache_file).unwrap();
843 assert!(!data.is_empty());
844 data[0] = 255; std::fs::write(&cache_file, &data).unwrap();
846
847 let result = CacheStore::load(&dir);
849 assert!(result.is_none());
850
851 let _ = std::fs::remove_dir_all(&dir);
852 }
853
854 #[test]
855 fn module_to_cached_roundtrip_type_only_import() {
856 let module = ModuleInfo {
857 file_id: FileId(0),
858 exports: vec![],
859 imports: vec![ImportInfo {
860 source: "./types".to_string(),
861 imported_name: ImportedName::Named("Foo".to_string()),
862 local_name: "Foo".to_string(),
863 is_type_only: true,
864 span: Span::new(0, 10),
865 }],
866 re_exports: vec![],
867 dynamic_imports: vec![],
868 require_calls: vec![],
869 member_accesses: vec![],
870 whole_object_uses: vec![],
871 dynamic_import_patterns: vec![],
872 has_cjs_exports: false,
873 content_hash: 0,
874 };
875
876 let cached = module_to_cached(&module);
877 let restored = cached_to_module(&cached, FileId(0));
878
879 assert!(restored.imports[0].is_type_only);
880 assert_eq!(restored.imports[0].span.start, 0);
881 assert_eq!(restored.imports[0].span.end, 10);
882 }
883}