1mod build;
7mod cycles;
8mod re_exports;
9mod reachability;
10pub mod types;
11
12use std::path::PathBuf;
13
14use fixedbitset::FixedBitSet;
15use rustc_hash::{FxHashMap, FxHashSet};
16
17use crate::resolve::ResolvedModule;
18use fallow_types::discover::{DiscoveredFile, EntryPoint, FileId};
19use fallow_types::extract::ImportedName;
20
21pub use types::{ExportSymbol, ModuleNode, ReExportEdge, ReferenceKind, SymbolReference};
23
24#[derive(Debug)]
26pub struct ModuleGraph {
27 pub modules: Vec<ModuleNode>,
29 edges: Vec<Edge>,
31 pub package_usage: FxHashMap<String, Vec<FileId>>,
33 pub type_only_package_usage: FxHashMap<String, Vec<FileId>>,
37 pub entry_points: FxHashSet<FileId>,
39 pub reverse_deps: Vec<Vec<FileId>>,
41 namespace_imported: FixedBitSet,
43}
44
45#[derive(Debug)]
47pub(super) struct Edge {
48 pub(super) source: FileId,
49 pub(super) target: FileId,
50 pub(super) symbols: Vec<ImportedSymbol>,
51}
52
53#[derive(Debug)]
55pub(super) struct ImportedSymbol {
56 pub(super) imported_name: ImportedName,
57 pub(super) local_name: String,
58 pub(super) import_span: oxc_span::Span,
60}
61
62#[cfg(target_pointer_width = "64")]
66const _: () = assert!(std::mem::size_of::<Edge>() == 32);
67#[cfg(target_pointer_width = "64")]
68const _: () = assert!(std::mem::size_of::<ImportedSymbol>() == 56);
69
70impl ModuleGraph {
71 pub fn build(
73 resolved_modules: &[ResolvedModule],
74 entry_points: &[EntryPoint],
75 files: &[DiscoveredFile],
76 ) -> Self {
77 let _span = tracing::info_span!("build_graph").entered();
78
79 let module_count = files.len();
80
81 let max_file_id = files
84 .iter()
85 .map(|f| f.id.0 as usize)
86 .max()
87 .map_or(0, |m| m + 1);
88 let total_capacity = max_file_id.max(module_count);
89
90 let path_to_id: FxHashMap<PathBuf, FileId> =
92 files.iter().map(|f| (f.path.clone(), f.id)).collect();
93
94 let module_by_id: FxHashMap<FileId, &ResolvedModule> =
96 resolved_modules.iter().map(|m| (m.file_id, m)).collect();
97
98 let entry_point_ids: FxHashSet<FileId> = entry_points
100 .iter()
101 .filter_map(|ep| {
102 path_to_id.get(&ep.path).copied().or_else(|| {
104 ep.path
106 .canonicalize()
107 .ok()
108 .and_then(|c| path_to_id.get(&c).copied())
109 })
110 })
111 .collect();
112
113 let mut graph = Self::populate_edges(
115 files,
116 &module_by_id,
117 &entry_point_ids,
118 module_count,
119 total_capacity,
120 );
121
122 graph.populate_references(&module_by_id, &entry_point_ids);
124
125 graph.mark_reachable(total_capacity);
127
128 graph.resolve_re_export_chains();
130
131 graph
132 }
133
134 #[must_use]
136 pub const fn module_count(&self) -> usize {
137 self.modules.len()
138 }
139
140 #[must_use]
142 pub const fn edge_count(&self) -> usize {
143 self.edges.len()
144 }
145
146 #[must_use]
149 pub fn has_namespace_import(&self, file_id: FileId) -> bool {
150 let idx = file_id.0 as usize;
151 if idx >= self.namespace_imported.len() {
152 return false;
153 }
154 self.namespace_imported.contains(idx)
155 }
156
157 #[must_use]
159 pub fn edges_for(&self, file_id: FileId) -> Vec<FileId> {
160 let idx = file_id.0 as usize;
161 if idx >= self.modules.len() {
162 return Vec::new();
163 }
164 let range = &self.modules[idx].edge_range;
165 self.edges[range.clone()].iter().map(|e| e.target).collect()
166 }
167
168 #[must_use]
171 pub fn find_import_span_start(&self, source: FileId, target: FileId) -> Option<u32> {
172 let idx = source.0 as usize;
173 if idx >= self.modules.len() {
174 return None;
175 }
176 let range = &self.modules[idx].edge_range;
177 for edge in &self.edges[range.clone()] {
178 if edge.target == target {
179 return edge.symbols.first().map(|s| s.import_span.start);
180 }
181 }
182 None
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule};
190 use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
191 use fallow_types::extract::{ExportName, ImportInfo, ImportedName};
192 use std::path::PathBuf;
193
194 fn build_simple_graph() -> ModuleGraph {
196 let files = vec![
198 DiscoveredFile {
199 id: FileId(0),
200 path: PathBuf::from("/project/src/entry.ts"),
201 size_bytes: 100,
202 },
203 DiscoveredFile {
204 id: FileId(1),
205 path: PathBuf::from("/project/src/utils.ts"),
206 size_bytes: 50,
207 },
208 ];
209
210 let entry_points = vec![EntryPoint {
211 path: PathBuf::from("/project/src/entry.ts"),
212 source: EntryPointSource::PackageJsonMain,
213 }];
214
215 let resolved_modules = vec![
216 ResolvedModule {
217 file_id: FileId(0),
218 path: PathBuf::from("/project/src/entry.ts"),
219 exports: vec![],
220 re_exports: vec![],
221 resolved_imports: vec![ResolvedImport {
222 info: ImportInfo {
223 source: "./utils".to_string(),
224 imported_name: ImportedName::Named("foo".to_string()),
225 local_name: "foo".to_string(),
226 is_type_only: false,
227 span: oxc_span::Span::new(0, 10),
228 source_span: oxc_span::Span::default(),
229 },
230 target: ResolveResult::InternalModule(FileId(1)),
231 }],
232 resolved_dynamic_imports: vec![],
233 resolved_dynamic_patterns: vec![],
234 member_accesses: vec![],
235 whole_object_uses: vec![],
236 has_cjs_exports: false,
237 unused_import_bindings: FxHashSet::default(),
238 },
239 ResolvedModule {
240 file_id: FileId(1),
241 path: PathBuf::from("/project/src/utils.ts"),
242 exports: vec![
243 fallow_types::extract::ExportInfo {
244 name: ExportName::Named("foo".to_string()),
245 local_name: Some("foo".to_string()),
246 is_type_only: false,
247 is_public: false,
248 span: oxc_span::Span::new(0, 20),
249 members: vec![],
250 },
251 fallow_types::extract::ExportInfo {
252 name: ExportName::Named("bar".to_string()),
253 local_name: Some("bar".to_string()),
254 is_type_only: false,
255 is_public: false,
256 span: oxc_span::Span::new(25, 45),
257 members: vec![],
258 },
259 ],
260 re_exports: vec![],
261 resolved_imports: vec![],
262 resolved_dynamic_imports: vec![],
263 resolved_dynamic_patterns: vec![],
264 member_accesses: vec![],
265 whole_object_uses: vec![],
266 has_cjs_exports: false,
267 unused_import_bindings: FxHashSet::default(),
268 },
269 ];
270
271 ModuleGraph::build(&resolved_modules, &entry_points, &files)
272 }
273
274 #[test]
275 fn graph_module_count() {
276 let graph = build_simple_graph();
277 assert_eq!(graph.module_count(), 2);
278 }
279
280 #[test]
281 fn graph_edge_count() {
282 let graph = build_simple_graph();
283 assert_eq!(graph.edge_count(), 1);
284 }
285
286 #[test]
287 fn graph_entry_point_is_reachable() {
288 let graph = build_simple_graph();
289 assert!(graph.modules[0].is_entry_point);
290 assert!(graph.modules[0].is_reachable);
291 }
292
293 #[test]
294 fn graph_imported_module_is_reachable() {
295 let graph = build_simple_graph();
296 assert!(!graph.modules[1].is_entry_point);
297 assert!(graph.modules[1].is_reachable);
298 }
299
300 #[test]
301 fn graph_export_has_reference() {
302 let graph = build_simple_graph();
303 let utils = &graph.modules[1];
304 let foo_export = utils
305 .exports
306 .iter()
307 .find(|e| e.name.to_string() == "foo")
308 .unwrap();
309 assert!(
310 !foo_export.references.is_empty(),
311 "foo should have references"
312 );
313 }
314
315 #[test]
316 fn graph_unused_export_no_reference() {
317 let graph = build_simple_graph();
318 let utils = &graph.modules[1];
319 let bar_export = utils
320 .exports
321 .iter()
322 .find(|e| e.name.to_string() == "bar")
323 .unwrap();
324 assert!(
325 bar_export.references.is_empty(),
326 "bar should have no references"
327 );
328 }
329
330 #[test]
331 fn graph_no_namespace_import() {
332 let graph = build_simple_graph();
333 assert!(!graph.has_namespace_import(FileId(0)));
334 assert!(!graph.has_namespace_import(FileId(1)));
335 }
336
337 #[test]
338 fn graph_has_namespace_import() {
339 let files = vec![
340 DiscoveredFile {
341 id: FileId(0),
342 path: PathBuf::from("/project/entry.ts"),
343 size_bytes: 100,
344 },
345 DiscoveredFile {
346 id: FileId(1),
347 path: PathBuf::from("/project/utils.ts"),
348 size_bytes: 50,
349 },
350 ];
351
352 let entry_points = vec![EntryPoint {
353 path: PathBuf::from("/project/entry.ts"),
354 source: EntryPointSource::PackageJsonMain,
355 }];
356
357 let resolved_modules = vec![
358 ResolvedModule {
359 file_id: FileId(0),
360 path: PathBuf::from("/project/entry.ts"),
361 exports: vec![],
362 re_exports: vec![],
363 resolved_imports: vec![ResolvedImport {
364 info: ImportInfo {
365 source: "./utils".to_string(),
366 imported_name: ImportedName::Namespace,
367 local_name: "utils".to_string(),
368 is_type_only: false,
369 span: oxc_span::Span::new(0, 10),
370 source_span: oxc_span::Span::default(),
371 },
372 target: ResolveResult::InternalModule(FileId(1)),
373 }],
374 resolved_dynamic_imports: vec![],
375 resolved_dynamic_patterns: vec![],
376 member_accesses: vec![],
377 whole_object_uses: vec![],
378 has_cjs_exports: false,
379 unused_import_bindings: FxHashSet::default(),
380 },
381 ResolvedModule {
382 file_id: FileId(1),
383 path: PathBuf::from("/project/utils.ts"),
384 exports: vec![fallow_types::extract::ExportInfo {
385 name: ExportName::Named("foo".to_string()),
386 local_name: Some("foo".to_string()),
387 is_type_only: false,
388 is_public: false,
389 span: oxc_span::Span::new(0, 20),
390 members: vec![],
391 }],
392 re_exports: vec![],
393 resolved_imports: vec![],
394 resolved_dynamic_imports: vec![],
395 resolved_dynamic_patterns: vec![],
396 member_accesses: vec![],
397 whole_object_uses: vec![],
398 has_cjs_exports: false,
399 unused_import_bindings: FxHashSet::default(),
400 },
401 ];
402
403 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
404 assert!(
405 graph.has_namespace_import(FileId(1)),
406 "utils should have namespace import"
407 );
408 }
409
410 #[test]
411 fn graph_has_namespace_import_out_of_bounds() {
412 let graph = build_simple_graph();
413 assert!(!graph.has_namespace_import(FileId(999)));
414 }
415
416 #[test]
417 fn graph_unreachable_module() {
418 let files = vec![
420 DiscoveredFile {
421 id: FileId(0),
422 path: PathBuf::from("/project/entry.ts"),
423 size_bytes: 100,
424 },
425 DiscoveredFile {
426 id: FileId(1),
427 path: PathBuf::from("/project/utils.ts"),
428 size_bytes: 50,
429 },
430 DiscoveredFile {
431 id: FileId(2),
432 path: PathBuf::from("/project/orphan.ts"),
433 size_bytes: 30,
434 },
435 ];
436
437 let entry_points = vec![EntryPoint {
438 path: PathBuf::from("/project/entry.ts"),
439 source: EntryPointSource::PackageJsonMain,
440 }];
441
442 let resolved_modules = vec![
443 ResolvedModule {
444 file_id: FileId(0),
445 path: PathBuf::from("/project/entry.ts"),
446 exports: vec![],
447 re_exports: vec![],
448 resolved_imports: vec![ResolvedImport {
449 info: ImportInfo {
450 source: "./utils".to_string(),
451 imported_name: ImportedName::Named("foo".to_string()),
452 local_name: "foo".to_string(),
453 is_type_only: false,
454 span: oxc_span::Span::new(0, 10),
455 source_span: oxc_span::Span::default(),
456 },
457 target: ResolveResult::InternalModule(FileId(1)),
458 }],
459 resolved_dynamic_imports: vec![],
460 resolved_dynamic_patterns: vec![],
461 member_accesses: vec![],
462 whole_object_uses: vec![],
463 has_cjs_exports: false,
464 unused_import_bindings: FxHashSet::default(),
465 },
466 ResolvedModule {
467 file_id: FileId(1),
468 path: PathBuf::from("/project/utils.ts"),
469 exports: vec![fallow_types::extract::ExportInfo {
470 name: ExportName::Named("foo".to_string()),
471 local_name: Some("foo".to_string()),
472 is_type_only: false,
473 is_public: false,
474 span: oxc_span::Span::new(0, 20),
475 members: vec![],
476 }],
477 re_exports: vec![],
478 resolved_imports: vec![],
479 resolved_dynamic_imports: vec![],
480 resolved_dynamic_patterns: vec![],
481 member_accesses: vec![],
482 whole_object_uses: vec![],
483 has_cjs_exports: false,
484 unused_import_bindings: FxHashSet::default(),
485 },
486 ResolvedModule {
487 file_id: FileId(2),
488 path: PathBuf::from("/project/orphan.ts"),
489 exports: vec![fallow_types::extract::ExportInfo {
490 name: ExportName::Named("orphan".to_string()),
491 local_name: Some("orphan".to_string()),
492 is_type_only: false,
493 is_public: false,
494 span: oxc_span::Span::new(0, 20),
495 members: vec![],
496 }],
497 re_exports: vec![],
498 resolved_imports: vec![],
499 resolved_dynamic_imports: vec![],
500 resolved_dynamic_patterns: vec![],
501 member_accesses: vec![],
502 whole_object_uses: vec![],
503 has_cjs_exports: false,
504 unused_import_bindings: FxHashSet::default(),
505 },
506 ];
507
508 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
509
510 assert!(graph.modules[0].is_reachable, "entry should be reachable");
511 assert!(graph.modules[1].is_reachable, "utils should be reachable");
512 assert!(
513 !graph.modules[2].is_reachable,
514 "orphan should NOT be reachable"
515 );
516 }
517
518 #[test]
519 fn graph_package_usage_tracked() {
520 let files = vec![DiscoveredFile {
521 id: FileId(0),
522 path: PathBuf::from("/project/entry.ts"),
523 size_bytes: 100,
524 }];
525
526 let entry_points = vec![EntryPoint {
527 path: PathBuf::from("/project/entry.ts"),
528 source: EntryPointSource::PackageJsonMain,
529 }];
530
531 let resolved_modules = vec![ResolvedModule {
532 file_id: FileId(0),
533 path: PathBuf::from("/project/entry.ts"),
534 exports: vec![],
535 re_exports: vec![],
536 resolved_imports: vec![
537 ResolvedImport {
538 info: ImportInfo {
539 source: "react".to_string(),
540 imported_name: ImportedName::Default,
541 local_name: "React".to_string(),
542 is_type_only: false,
543 span: oxc_span::Span::new(0, 10),
544 source_span: oxc_span::Span::default(),
545 },
546 target: ResolveResult::NpmPackage("react".to_string()),
547 },
548 ResolvedImport {
549 info: ImportInfo {
550 source: "lodash".to_string(),
551 imported_name: ImportedName::Named("merge".to_string()),
552 local_name: "merge".to_string(),
553 is_type_only: false,
554 span: oxc_span::Span::new(15, 30),
555 source_span: oxc_span::Span::default(),
556 },
557 target: ResolveResult::NpmPackage("lodash".to_string()),
558 },
559 ],
560 resolved_dynamic_imports: vec![],
561 resolved_dynamic_patterns: vec![],
562 member_accesses: vec![],
563 whole_object_uses: vec![],
564 has_cjs_exports: false,
565 unused_import_bindings: FxHashSet::default(),
566 }];
567
568 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
569 assert!(graph.package_usage.contains_key("react"));
570 assert!(graph.package_usage.contains_key("lodash"));
571 assert!(!graph.package_usage.contains_key("express"));
572 }
573
574 #[test]
575 fn graph_empty() {
576 let graph = ModuleGraph::build(&[], &[], &[]);
577 assert_eq!(graph.module_count(), 0);
578 assert_eq!(graph.edge_count(), 0);
579 }
580
581 #[test]
582 fn graph_cjs_exports_tracked() {
583 let files = vec![DiscoveredFile {
584 id: FileId(0),
585 path: PathBuf::from("/project/entry.ts"),
586 size_bytes: 100,
587 }];
588
589 let entry_points = vec![EntryPoint {
590 path: PathBuf::from("/project/entry.ts"),
591 source: EntryPointSource::PackageJsonMain,
592 }];
593
594 let resolved_modules = vec![ResolvedModule {
595 file_id: FileId(0),
596 path: PathBuf::from("/project/entry.ts"),
597 exports: vec![],
598 re_exports: vec![],
599 resolved_imports: vec![],
600 resolved_dynamic_imports: vec![],
601 resolved_dynamic_patterns: vec![],
602 member_accesses: vec![],
603 whole_object_uses: vec![],
604 has_cjs_exports: true,
605 unused_import_bindings: FxHashSet::default(),
606 }];
607
608 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
609 assert!(graph.modules[0].has_cjs_exports);
610 }
611
612 #[test]
613 fn graph_edges_for_returns_targets() {
614 let graph = build_simple_graph();
615 let targets = graph.edges_for(FileId(0));
616 assert_eq!(targets, vec![FileId(1)]);
617 }
618
619 #[test]
620 fn graph_edges_for_no_imports() {
621 let graph = build_simple_graph();
622 let targets = graph.edges_for(FileId(1));
624 assert!(targets.is_empty());
625 }
626
627 #[test]
628 fn graph_edges_for_out_of_bounds() {
629 let graph = build_simple_graph();
630 let targets = graph.edges_for(FileId(999));
631 assert!(targets.is_empty());
632 }
633
634 #[test]
635 fn graph_find_import_span_start_found() {
636 let graph = build_simple_graph();
637 let span_start = graph.find_import_span_start(FileId(0), FileId(1));
638 assert!(span_start.is_some());
639 assert_eq!(span_start.unwrap(), 0);
640 }
641
642 #[test]
643 fn graph_find_import_span_start_wrong_target() {
644 let graph = build_simple_graph();
645 let span_start = graph.find_import_span_start(FileId(0), FileId(0));
647 assert!(span_start.is_none());
648 }
649
650 #[test]
651 fn graph_find_import_span_start_source_out_of_bounds() {
652 let graph = build_simple_graph();
653 let span_start = graph.find_import_span_start(FileId(999), FileId(1));
654 assert!(span_start.is_none());
655 }
656
657 #[test]
658 fn graph_find_import_span_start_no_edges() {
659 let graph = build_simple_graph();
660 let span_start = graph.find_import_span_start(FileId(1), FileId(0));
662 assert!(span_start.is_none());
663 }
664
665 #[test]
666 fn graph_reverse_deps_populated() {
667 let graph = build_simple_graph();
668 assert!(graph.reverse_deps[1].contains(&FileId(0)));
670 assert!(graph.reverse_deps[0].is_empty());
672 }
673
674 #[test]
675 fn graph_type_only_package_usage_tracked() {
676 let files = vec![DiscoveredFile {
677 id: FileId(0),
678 path: PathBuf::from("/project/entry.ts"),
679 size_bytes: 100,
680 }];
681 let entry_points = vec![EntryPoint {
682 path: PathBuf::from("/project/entry.ts"),
683 source: EntryPointSource::PackageJsonMain,
684 }];
685 let resolved_modules = vec![ResolvedModule {
686 file_id: FileId(0),
687 path: PathBuf::from("/project/entry.ts"),
688 exports: vec![],
689 re_exports: vec![],
690 resolved_imports: vec![
691 ResolvedImport {
692 info: ImportInfo {
693 source: "react".to_string(),
694 imported_name: ImportedName::Named("FC".to_string()),
695 local_name: "FC".to_string(),
696 is_type_only: true,
697 span: oxc_span::Span::new(0, 10),
698 source_span: oxc_span::Span::default(),
699 },
700 target: ResolveResult::NpmPackage("react".to_string()),
701 },
702 ResolvedImport {
703 info: ImportInfo {
704 source: "react".to_string(),
705 imported_name: ImportedName::Named("useState".to_string()),
706 local_name: "useState".to_string(),
707 is_type_only: false,
708 span: oxc_span::Span::new(15, 30),
709 source_span: oxc_span::Span::default(),
710 },
711 target: ResolveResult::NpmPackage("react".to_string()),
712 },
713 ],
714 resolved_dynamic_imports: vec![],
715 resolved_dynamic_patterns: vec![],
716 member_accesses: vec![],
717 whole_object_uses: vec![],
718 has_cjs_exports: false,
719 unused_import_bindings: FxHashSet::default(),
720 }];
721
722 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
723 assert!(graph.package_usage.contains_key("react"));
724 assert!(graph.type_only_package_usage.contains_key("react"));
725 }
726
727 #[test]
728 fn graph_default_import_reference() {
729 let files = vec![
730 DiscoveredFile {
731 id: FileId(0),
732 path: PathBuf::from("/project/entry.ts"),
733 size_bytes: 100,
734 },
735 DiscoveredFile {
736 id: FileId(1),
737 path: PathBuf::from("/project/utils.ts"),
738 size_bytes: 50,
739 },
740 ];
741 let entry_points = vec![EntryPoint {
742 path: PathBuf::from("/project/entry.ts"),
743 source: EntryPointSource::PackageJsonMain,
744 }];
745 let resolved_modules = vec![
746 ResolvedModule {
747 file_id: FileId(0),
748 path: PathBuf::from("/project/entry.ts"),
749 exports: vec![],
750 re_exports: vec![],
751 resolved_imports: vec![ResolvedImport {
752 info: ImportInfo {
753 source: "./utils".to_string(),
754 imported_name: ImportedName::Default,
755 local_name: "Utils".to_string(),
756 is_type_only: false,
757 span: oxc_span::Span::new(0, 10),
758 source_span: oxc_span::Span::default(),
759 },
760 target: ResolveResult::InternalModule(FileId(1)),
761 }],
762 resolved_dynamic_imports: vec![],
763 resolved_dynamic_patterns: vec![],
764 member_accesses: vec![],
765 whole_object_uses: vec![],
766 has_cjs_exports: false,
767 unused_import_bindings: FxHashSet::default(),
768 },
769 ResolvedModule {
770 file_id: FileId(1),
771 path: PathBuf::from("/project/utils.ts"),
772 exports: vec![fallow_types::extract::ExportInfo {
773 name: ExportName::Default,
774 local_name: None,
775 is_type_only: false,
776 is_public: false,
777 span: oxc_span::Span::new(0, 20),
778 members: vec![],
779 }],
780 re_exports: vec![],
781 resolved_imports: vec![],
782 resolved_dynamic_imports: vec![],
783 resolved_dynamic_patterns: vec![],
784 member_accesses: vec![],
785 whole_object_uses: vec![],
786 has_cjs_exports: false,
787 unused_import_bindings: FxHashSet::default(),
788 },
789 ];
790
791 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
792 let utils = &graph.modules[1];
793 let default_export = utils
794 .exports
795 .iter()
796 .find(|e| matches!(e.name, ExportName::Default))
797 .unwrap();
798 assert!(!default_export.references.is_empty());
799 assert_eq!(
800 default_export.references[0].kind,
801 ReferenceKind::DefaultImport
802 );
803 }
804
805 #[test]
806 fn graph_side_effect_import_no_export_reference() {
807 let files = vec![
808 DiscoveredFile {
809 id: FileId(0),
810 path: PathBuf::from("/project/entry.ts"),
811 size_bytes: 100,
812 },
813 DiscoveredFile {
814 id: FileId(1),
815 path: PathBuf::from("/project/styles.ts"),
816 size_bytes: 50,
817 },
818 ];
819 let entry_points = vec![EntryPoint {
820 path: PathBuf::from("/project/entry.ts"),
821 source: EntryPointSource::PackageJsonMain,
822 }];
823 let resolved_modules = vec![
824 ResolvedModule {
825 file_id: FileId(0),
826 path: PathBuf::from("/project/entry.ts"),
827 exports: vec![],
828 re_exports: vec![],
829 resolved_imports: vec![ResolvedImport {
830 info: ImportInfo {
831 source: "./styles".to_string(),
832 imported_name: ImportedName::SideEffect,
833 local_name: String::new(),
834 is_type_only: false,
835 span: oxc_span::Span::new(0, 10),
836 source_span: oxc_span::Span::default(),
837 },
838 target: ResolveResult::InternalModule(FileId(1)),
839 }],
840 resolved_dynamic_imports: vec![],
841 resolved_dynamic_patterns: vec![],
842 member_accesses: vec![],
843 whole_object_uses: vec![],
844 has_cjs_exports: false,
845 unused_import_bindings: FxHashSet::default(),
846 },
847 ResolvedModule {
848 file_id: FileId(1),
849 path: PathBuf::from("/project/styles.ts"),
850 exports: vec![fallow_types::extract::ExportInfo {
851 name: ExportName::Named("primaryColor".to_string()),
852 local_name: Some("primaryColor".to_string()),
853 is_type_only: false,
854 is_public: false,
855 span: oxc_span::Span::new(0, 20),
856 members: vec![],
857 }],
858 re_exports: vec![],
859 resolved_imports: vec![],
860 resolved_dynamic_imports: vec![],
861 resolved_dynamic_patterns: vec![],
862 member_accesses: vec![],
863 whole_object_uses: vec![],
864 has_cjs_exports: false,
865 unused_import_bindings: FxHashSet::default(),
866 },
867 ];
868
869 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
870 assert_eq!(graph.edge_count(), 1);
872 let styles = &graph.modules[1];
873 let export = &styles.exports[0];
874 assert!(
876 export.references.is_empty(),
877 "side-effect import should not reference named exports"
878 );
879 }
880
881 #[test]
882 fn graph_multiple_entry_points() {
883 let files = vec![
884 DiscoveredFile {
885 id: FileId(0),
886 path: PathBuf::from("/project/main.ts"),
887 size_bytes: 100,
888 },
889 DiscoveredFile {
890 id: FileId(1),
891 path: PathBuf::from("/project/worker.ts"),
892 size_bytes: 100,
893 },
894 DiscoveredFile {
895 id: FileId(2),
896 path: PathBuf::from("/project/shared.ts"),
897 size_bytes: 50,
898 },
899 ];
900 let entry_points = vec![
901 EntryPoint {
902 path: PathBuf::from("/project/main.ts"),
903 source: EntryPointSource::PackageJsonMain,
904 },
905 EntryPoint {
906 path: PathBuf::from("/project/worker.ts"),
907 source: EntryPointSource::PackageJsonMain,
908 },
909 ];
910 let resolved_modules = vec![
911 ResolvedModule {
912 file_id: FileId(0),
913 path: PathBuf::from("/project/main.ts"),
914 exports: vec![],
915 re_exports: vec![],
916 resolved_imports: vec![ResolvedImport {
917 info: ImportInfo {
918 source: "./shared".to_string(),
919 imported_name: ImportedName::Named("helper".to_string()),
920 local_name: "helper".to_string(),
921 is_type_only: false,
922 span: oxc_span::Span::new(0, 10),
923 source_span: oxc_span::Span::default(),
924 },
925 target: ResolveResult::InternalModule(FileId(2)),
926 }],
927 resolved_dynamic_imports: vec![],
928 resolved_dynamic_patterns: vec![],
929 member_accesses: vec![],
930 whole_object_uses: vec![],
931 has_cjs_exports: false,
932 unused_import_bindings: FxHashSet::default(),
933 },
934 ResolvedModule {
935 file_id: FileId(1),
936 path: PathBuf::from("/project/worker.ts"),
937 exports: vec![],
938 re_exports: vec![],
939 resolved_imports: vec![],
940 resolved_dynamic_imports: vec![],
941 resolved_dynamic_patterns: vec![],
942 member_accesses: vec![],
943 whole_object_uses: vec![],
944 has_cjs_exports: false,
945 unused_import_bindings: FxHashSet::default(),
946 },
947 ResolvedModule {
948 file_id: FileId(2),
949 path: PathBuf::from("/project/shared.ts"),
950 exports: vec![fallow_types::extract::ExportInfo {
951 name: ExportName::Named("helper".to_string()),
952 local_name: Some("helper".to_string()),
953 is_type_only: false,
954 is_public: false,
955 span: oxc_span::Span::new(0, 20),
956 members: vec![],
957 }],
958 re_exports: vec![],
959 resolved_imports: vec![],
960 resolved_dynamic_imports: vec![],
961 resolved_dynamic_patterns: vec![],
962 member_accesses: vec![],
963 whole_object_uses: vec![],
964 has_cjs_exports: false,
965 unused_import_bindings: FxHashSet::default(),
966 },
967 ];
968
969 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
970 assert!(graph.modules[0].is_entry_point);
971 assert!(graph.modules[1].is_entry_point);
972 assert!(!graph.modules[2].is_entry_point);
973 assert!(graph.modules[0].is_reachable);
975 assert!(graph.modules[1].is_reachable);
976 assert!(graph.modules[2].is_reachable);
977 }
978}