1use std::path::Path;
2use std::sync::LazyLock;
3
4use fallow_config::ResolvedConfig;
5use oxc_allocator::Allocator;
6use oxc_ast::ast::*;
7use oxc_ast_visit::Visit;
8use oxc_ast_visit::walk;
9use oxc_parser::Parser;
10use oxc_span::{SourceType, Span};
11use rayon::prelude::*;
12
13use crate::cache::CacheStore;
14use crate::discover::{DiscoveredFile, FileId};
15
16#[derive(Debug, Clone)]
18pub struct ModuleInfo {
19 pub file_id: FileId,
20 pub exports: Vec<ExportInfo>,
21 pub imports: Vec<ImportInfo>,
22 pub re_exports: Vec<ReExportInfo>,
23 pub dynamic_imports: Vec<DynamicImportInfo>,
24 pub dynamic_import_patterns: Vec<DynamicImportPattern>,
25 pub require_calls: Vec<RequireCallInfo>,
26 pub member_accesses: Vec<MemberAccess>,
27 pub whole_object_uses: Vec<String>,
30 pub has_cjs_exports: bool,
31 pub content_hash: u64,
32}
33
34#[derive(Debug, Clone)]
36pub struct DynamicImportPattern {
37 pub prefix: String,
39 pub suffix: Option<String>,
41 pub span: Span,
42}
43
44#[derive(Debug, Clone, serde::Serialize)]
46pub struct ExportInfo {
47 pub name: ExportName,
48 pub local_name: Option<String>,
49 pub is_type_only: bool,
50 #[serde(serialize_with = "serialize_span")]
51 pub span: Span,
52 #[serde(default, skip_serializing_if = "Vec::is_empty")]
54 pub members: Vec<MemberInfo>,
55}
56
57#[derive(Debug, Clone, serde::Serialize)]
59pub struct MemberInfo {
60 pub name: String,
61 pub kind: MemberKind,
62 #[serde(serialize_with = "serialize_span")]
63 pub span: Span,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
68#[serde(rename_all = "snake_case")]
69pub enum MemberKind {
70 EnumMember,
71 ClassMethod,
72 ClassProperty,
73}
74
75#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
77pub struct MemberAccess {
78 pub object: String,
80 pub member: String,
82}
83
84fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
85 use serde::ser::SerializeMap;
86 let mut map = serializer.serialize_map(Some(2))?;
87 map.serialize_entry("start", &span.start)?;
88 map.serialize_entry("end", &span.end)?;
89 map.end()
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
94pub enum ExportName {
95 Named(String),
96 Default,
97}
98
99impl std::fmt::Display for ExportName {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 Self::Named(n) => write!(f, "{n}"),
103 Self::Default => write!(f, "default"),
104 }
105 }
106}
107
108#[derive(Debug, Clone)]
110pub struct ImportInfo {
111 pub source: String,
112 pub imported_name: ImportedName,
113 pub local_name: String,
114 pub is_type_only: bool,
115 pub span: Span,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum ImportedName {
121 Named(String),
122 Default,
123 Namespace,
124 SideEffect,
125}
126
127#[derive(Debug, Clone)]
129pub struct ReExportInfo {
130 pub source: String,
131 pub imported_name: String,
132 pub exported_name: String,
133 pub is_type_only: bool,
134}
135
136#[derive(Debug, Clone)]
138pub struct DynamicImportInfo {
139 pub source: String,
140 pub span: Span,
141}
142
143#[derive(Debug, Clone)]
145pub struct RequireCallInfo {
146 pub source: String,
147 pub span: Span,
148 pub destructured_names: Vec<String>,
152 pub local_name: Option<String>,
155}
156
157pub fn parse_all_files(
160 files: &[DiscoveredFile],
161 _config: &ResolvedConfig,
162 cache: Option<&CacheStore>,
163) -> Vec<ModuleInfo> {
164 use std::sync::atomic::{AtomicUsize, Ordering};
165 let cache_hits = AtomicUsize::new(0);
166 let cache_misses = AtomicUsize::new(0);
167
168 let result: Vec<ModuleInfo> = files
169 .par_iter()
170 .filter_map(|file| parse_single_file_cached(file, cache, &cache_hits, &cache_misses))
171 .collect();
172
173 let hits = cache_hits.load(Ordering::Relaxed);
174 let misses = cache_misses.load(Ordering::Relaxed);
175 if hits > 0 || misses > 0 {
176 tracing::info!(
177 cache_hits = hits,
178 cache_misses = misses,
179 "incremental cache stats"
180 );
181 }
182
183 result
184}
185
186fn parse_single_file_cached(
188 file: &DiscoveredFile,
189 cache: Option<&CacheStore>,
190 cache_hits: &std::sync::atomic::AtomicUsize,
191 cache_misses: &std::sync::atomic::AtomicUsize,
192) -> Option<ModuleInfo> {
193 use std::sync::atomic::Ordering;
194
195 let source = std::fs::read_to_string(&file.path).ok()?;
196 let content_hash = xxhash_rust::xxh3::xxh3_64(source.as_bytes());
197
198 if let Some(store) = cache
200 && let Some(cached) = store.get(&file.path, content_hash)
201 {
202 cache_hits.fetch_add(1, Ordering::Relaxed);
203 return Some(crate::cache::cached_to_module(cached, file.id));
204 }
205 cache_misses.fetch_add(1, Ordering::Relaxed);
206
207 Some(parse_source_to_module(
209 file.id,
210 &file.path,
211 &source,
212 content_hash,
213 ))
214}
215
216pub fn parse_single_file(file: &DiscoveredFile) -> Option<ModuleInfo> {
218 let source = std::fs::read_to_string(&file.path).ok()?;
219 let content_hash = xxhash_rust::xxh3::xxh3_64(source.as_bytes());
220 Some(parse_source_to_module(
221 file.id,
222 &file.path,
223 &source,
224 content_hash,
225 ))
226}
227
228static SCRIPT_BLOCK_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
230 regex::Regex::new(r#"(?is)<script\b(?P<attrs>[^>]*)>(?P<body>[\s\S]*?)</script>"#)
231 .expect("valid regex")
232});
233
234static LANG_ATTR_RE: LazyLock<regex::Regex> =
236 LazyLock::new(|| regex::Regex::new(r#"lang\s*=\s*["'](\w+)["']"#).expect("valid regex"));
237
238pub(crate) struct SfcScript {
239 pub body: String,
240 pub is_typescript: bool,
241 pub byte_offset: usize,
243}
244
245pub(crate) fn extract_sfc_scripts(source: &str) -> Vec<SfcScript> {
246 SCRIPT_BLOCK_RE
247 .captures_iter(source)
248 .map(|cap| {
249 let attrs = cap.name("attrs").map(|m| m.as_str()).unwrap_or("");
250 let body_match = cap.name("body");
251 let byte_offset = body_match.map(|m| m.start()).unwrap_or(0);
252 let body = body_match.map(|m| m.as_str()).unwrap_or("").to_string();
253 let is_typescript = LANG_ATTR_RE
254 .captures(attrs)
255 .and_then(|c| c.get(1))
256 .map(|m| matches!(m.as_str(), "ts" | "tsx"))
257 .unwrap_or(false);
258 SfcScript {
259 body,
260 is_typescript,
261 byte_offset,
262 }
263 })
264 .collect()
265}
266
267pub(crate) fn is_sfc_file(path: &Path) -> bool {
268 path.extension()
269 .and_then(|e| e.to_str())
270 .is_some_and(|ext| ext == "vue" || ext == "svelte")
271}
272
273fn parse_sfc_to_module(file_id: FileId, source: &str, content_hash: u64) -> ModuleInfo {
275 let scripts = extract_sfc_scripts(source);
276
277 let mut combined = ModuleInfo {
278 file_id,
279 exports: Vec::new(),
280 imports: Vec::new(),
281 re_exports: Vec::new(),
282 dynamic_imports: Vec::new(),
283 dynamic_import_patterns: Vec::new(),
284 require_calls: Vec::new(),
285 member_accesses: Vec::new(),
286 whole_object_uses: Vec::new(),
287 has_cjs_exports: false,
288 content_hash,
289 };
290
291 for script in &scripts {
292 let source_type = if script.is_typescript {
293 SourceType::ts()
294 } else {
295 SourceType::mjs()
296 };
297 let allocator = Allocator::default();
298 let parser_return = Parser::new(&allocator, &script.body, source_type).parse();
299 let mut extractor = ModuleInfoExtractor::new();
300 extractor.visit_program(&parser_return.program);
301
302 combined.imports.extend(extractor.imports);
303 combined.exports.extend(extractor.exports);
304 combined.re_exports.extend(extractor.re_exports);
305 combined.dynamic_imports.extend(extractor.dynamic_imports);
306 combined
307 .dynamic_import_patterns
308 .extend(extractor.dynamic_import_patterns);
309 combined.require_calls.extend(extractor.require_calls);
310 combined.member_accesses.extend(extractor.member_accesses);
311 combined
312 .whole_object_uses
313 .extend(extractor.whole_object_uses);
314 combined.has_cjs_exports |= extractor.has_cjs_exports;
315 }
316
317 combined
318}
319
320fn parse_source_to_module(
322 file_id: FileId,
323 path: &Path,
324 source: &str,
325 content_hash: u64,
326) -> ModuleInfo {
327 if is_sfc_file(path) {
328 return parse_sfc_to_module(file_id, source, content_hash);
329 }
330
331 let source_type = SourceType::from_path(path).unwrap_or_default();
332 let allocator = Allocator::default();
333 let parser_return = Parser::new(&allocator, source, source_type).parse();
334
335 let mut extractor = ModuleInfoExtractor::new();
337 extractor.visit_program(&parser_return.program);
338
339 ModuleInfo {
340 file_id,
341 exports: extractor.exports,
342 imports: extractor.imports,
343 re_exports: extractor.re_exports,
344 dynamic_imports: extractor.dynamic_imports,
345 dynamic_import_patterns: extractor.dynamic_import_patterns,
346 require_calls: extractor.require_calls,
347 member_accesses: extractor.member_accesses,
348 whole_object_uses: extractor.whole_object_uses,
349 has_cjs_exports: extractor.has_cjs_exports,
350 content_hash,
351 }
352}
353
354pub fn parse_from_content(file_id: FileId, path: &Path, content: &str) -> ModuleInfo {
356 let content_hash = xxhash_rust::xxh3::xxh3_64(content.as_bytes());
357 parse_source_to_module(file_id, path, content, content_hash)
358}
359
360fn extract_class_members(class: &Class<'_>) -> Vec<MemberInfo> {
362 let mut members = Vec::new();
363 for element in &class.body.body {
364 match element {
365 ClassElement::MethodDefinition(method) => {
366 if let Some(name) = method.key.static_name() {
367 let name_str = name.to_string();
368 if name_str != "constructor"
370 && !matches!(
371 method.accessibility,
372 Some(oxc_ast::ast::TSAccessibility::Private)
373 | Some(oxc_ast::ast::TSAccessibility::Protected)
374 )
375 {
376 members.push(MemberInfo {
377 name: name_str,
378 kind: MemberKind::ClassMethod,
379 span: method.span,
380 });
381 }
382 }
383 }
384 ClassElement::PropertyDefinition(prop) => {
385 if let Some(name) = prop.key.static_name()
386 && !matches!(
387 prop.accessibility,
388 Some(oxc_ast::ast::TSAccessibility::Private)
389 | Some(oxc_ast::ast::TSAccessibility::Protected)
390 )
391 {
392 members.push(MemberInfo {
393 name: name.to_string(),
394 kind: MemberKind::ClassProperty,
395 span: prop.span,
396 });
397 }
398 }
399 _ => {}
400 }
401 }
402 members
403}
404
405fn is_meta_url_arg(arg: &Argument<'_>) -> bool {
407 if let Argument::StaticMemberExpression(member) = arg
408 && member.property.name == "url"
409 && matches!(member.object, Expression::MetaProperty(_))
410 {
411 return true;
412 }
413 false
414}
415
416struct ModuleInfoExtractor {
418 exports: Vec<ExportInfo>,
419 imports: Vec<ImportInfo>,
420 re_exports: Vec<ReExportInfo>,
421 dynamic_imports: Vec<DynamicImportInfo>,
422 dynamic_import_patterns: Vec<DynamicImportPattern>,
423 require_calls: Vec<RequireCallInfo>,
424 member_accesses: Vec<MemberAccess>,
425 whole_object_uses: Vec<String>,
426 has_cjs_exports: bool,
427 handled_require_spans: Vec<Span>,
429}
430
431impl ModuleInfoExtractor {
432 fn new() -> Self {
433 Self {
434 exports: Vec::new(),
435 imports: Vec::new(),
436 re_exports: Vec::new(),
437 dynamic_imports: Vec::new(),
438 dynamic_import_patterns: Vec::new(),
439 require_calls: Vec::new(),
440 member_accesses: Vec::new(),
441 whole_object_uses: Vec::new(),
442 has_cjs_exports: false,
443 handled_require_spans: Vec::new(),
444 }
445 }
446
447 fn extract_declaration_exports(&mut self, decl: &Declaration<'_>, is_type_only: bool) {
448 match decl {
449 Declaration::VariableDeclaration(var) => {
450 for declarator in &var.declarations {
451 self.extract_binding_pattern_names(&declarator.id, is_type_only);
452 }
453 }
454 Declaration::FunctionDeclaration(func) => {
455 if let Some(id) = func.id.as_ref() {
456 self.exports.push(ExportInfo {
457 name: ExportName::Named(id.name.to_string()),
458 local_name: Some(id.name.to_string()),
459 is_type_only,
460 span: id.span,
461 members: vec![],
462 });
463 }
464 }
465 Declaration::ClassDeclaration(class) => {
466 if let Some(id) = class.id.as_ref() {
467 let members = extract_class_members(class);
468 self.exports.push(ExportInfo {
469 name: ExportName::Named(id.name.to_string()),
470 local_name: Some(id.name.to_string()),
471 is_type_only,
472 span: id.span,
473 members,
474 });
475 }
476 }
477 Declaration::TSTypeAliasDeclaration(alias) => {
478 self.exports.push(ExportInfo {
479 name: ExportName::Named(alias.id.name.to_string()),
480 local_name: Some(alias.id.name.to_string()),
481 is_type_only: true,
482 span: alias.id.span,
483 members: vec![],
484 });
485 }
486 Declaration::TSInterfaceDeclaration(iface) => {
487 self.exports.push(ExportInfo {
488 name: ExportName::Named(iface.id.name.to_string()),
489 local_name: Some(iface.id.name.to_string()),
490 is_type_only: true,
491 span: iface.id.span,
492 members: vec![],
493 });
494 }
495 Declaration::TSEnumDeclaration(enumd) => {
496 let members: Vec<MemberInfo> = enumd
497 .body
498 .members
499 .iter()
500 .filter_map(|member| {
501 let name = match &member.id {
502 TSEnumMemberName::Identifier(id) => id.name.to_string(),
503 TSEnumMemberName::String(s) | TSEnumMemberName::ComputedString(s) => {
504 s.value.to_string()
505 }
506 TSEnumMemberName::ComputedTemplateString(_) => return None,
507 };
508 Some(MemberInfo {
509 name,
510 kind: MemberKind::EnumMember,
511 span: member.span,
512 })
513 })
514 .collect();
515 self.exports.push(ExportInfo {
516 name: ExportName::Named(enumd.id.name.to_string()),
517 local_name: Some(enumd.id.name.to_string()),
518 is_type_only,
519 span: enumd.id.span,
520 members,
521 });
522 }
523 Declaration::TSModuleDeclaration(module) => match &module.id {
524 TSModuleDeclarationName::Identifier(id) => {
525 self.exports.push(ExportInfo {
526 name: ExportName::Named(id.name.to_string()),
527 local_name: Some(id.name.to_string()),
528 is_type_only: true,
529 span: id.span,
530 members: vec![],
531 });
532 }
533 TSModuleDeclarationName::StringLiteral(lit) => {
534 self.exports.push(ExportInfo {
535 name: ExportName::Named(lit.value.to_string()),
536 local_name: Some(lit.value.to_string()),
537 is_type_only: true,
538 span: lit.span,
539 members: vec![],
540 });
541 }
542 },
543 _ => {}
544 }
545 }
546
547 fn extract_binding_pattern_names(&mut self, pattern: &BindingPattern<'_>, is_type_only: bool) {
548 match pattern {
549 BindingPattern::BindingIdentifier(id) => {
550 self.exports.push(ExportInfo {
551 name: ExportName::Named(id.name.to_string()),
552 local_name: Some(id.name.to_string()),
553 is_type_only,
554 span: id.span,
555 members: vec![],
556 });
557 }
558 BindingPattern::ObjectPattern(obj) => {
559 for prop in &obj.properties {
560 self.extract_binding_pattern_names(&prop.value, is_type_only);
561 }
562 }
563 BindingPattern::ArrayPattern(arr) => {
564 for elem in arr.elements.iter().flatten() {
565 self.extract_binding_pattern_names(elem, is_type_only);
566 }
567 }
568 BindingPattern::AssignmentPattern(assign) => {
569 self.extract_binding_pattern_names(&assign.left, is_type_only);
570 }
571 }
572 }
573}
574
575impl<'a> Visit<'a> for ModuleInfoExtractor {
576 fn visit_import_declaration(&mut self, decl: &ImportDeclaration<'a>) {
577 let source = decl.source.value.to_string();
578 let is_type_only = decl.import_kind.is_type();
579
580 if let Some(specifiers) = &decl.specifiers {
581 for spec in specifiers {
582 match spec {
583 ImportDeclarationSpecifier::ImportSpecifier(s) => {
584 self.imports.push(ImportInfo {
585 source: source.clone(),
586 imported_name: ImportedName::Named(s.imported.name().to_string()),
587 local_name: s.local.name.to_string(),
588 is_type_only: is_type_only || s.import_kind.is_type(),
589 span: s.span,
590 });
591 }
592 ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
593 self.imports.push(ImportInfo {
594 source: source.clone(),
595 imported_name: ImportedName::Default,
596 local_name: s.local.name.to_string(),
597 is_type_only,
598 span: s.span,
599 });
600 }
601 ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
602 self.imports.push(ImportInfo {
603 source: source.clone(),
604 imported_name: ImportedName::Namespace,
605 local_name: s.local.name.to_string(),
606 is_type_only,
607 span: s.span,
608 });
609 }
610 }
611 }
612 } else {
613 self.imports.push(ImportInfo {
615 source,
616 imported_name: ImportedName::SideEffect,
617 local_name: String::new(),
618 is_type_only: false,
619 span: decl.span,
620 });
621 }
622 }
623
624 fn visit_export_named_declaration(&mut self, decl: &ExportNamedDeclaration<'a>) {
625 let is_type_only = decl.export_kind.is_type();
626
627 if let Some(source) = &decl.source {
628 for spec in &decl.specifiers {
630 self.re_exports.push(ReExportInfo {
631 source: source.value.to_string(),
632 imported_name: spec.local.name().to_string(),
633 exported_name: spec.exported.name().to_string(),
634 is_type_only: is_type_only || spec.export_kind.is_type(),
635 });
636 }
637 } else {
638 if let Some(declaration) = &decl.declaration {
640 self.extract_declaration_exports(declaration, is_type_only);
641 }
642 for spec in &decl.specifiers {
643 self.exports.push(ExportInfo {
644 name: ExportName::Named(spec.exported.name().to_string()),
645 local_name: Some(spec.local.name().to_string()),
646 is_type_only: is_type_only || spec.export_kind.is_type(),
647 span: spec.span,
648 members: vec![],
649 });
650 }
651 }
652
653 walk::walk_export_named_declaration(self, decl);
654 }
655
656 fn visit_export_default_declaration(&mut self, decl: &ExportDefaultDeclaration<'a>) {
657 self.exports.push(ExportInfo {
658 name: ExportName::Default,
659 local_name: None,
660 is_type_only: false,
661 span: decl.span,
662 members: vec![],
663 });
664
665 walk::walk_export_default_declaration(self, decl);
666 }
667
668 fn visit_export_all_declaration(&mut self, decl: &ExportAllDeclaration<'a>) {
669 let exported_name = decl
670 .exported
671 .as_ref()
672 .map(|e| e.name().to_string())
673 .unwrap_or_else(|| "*".to_string());
674
675 self.re_exports.push(ReExportInfo {
676 source: decl.source.value.to_string(),
677 imported_name: "*".to_string(),
678 exported_name,
679 is_type_only: decl.export_kind.is_type(),
680 });
681
682 walk::walk_export_all_declaration(self, decl);
683 }
684
685 fn visit_import_expression(&mut self, expr: &ImportExpression<'a>) {
686 match &expr.source {
687 Expression::StringLiteral(lit) => {
688 self.dynamic_imports.push(DynamicImportInfo {
689 source: lit.value.to_string(),
690 span: expr.span,
691 });
692 }
693 Expression::TemplateLiteral(tpl)
694 if !tpl.quasis.is_empty() && !tpl.expressions.is_empty() =>
695 {
696 let first_quasi = tpl.quasis[0].value.raw.to_string();
700 if first_quasi.starts_with("./") || first_quasi.starts_with("../") {
701 let prefix = if tpl.expressions.len() > 1 {
702 format!("{first_quasi}**/")
704 } else {
705 first_quasi
706 };
707 let suffix = if tpl.quasis.len() > 1 {
708 let last = &tpl.quasis[tpl.quasis.len() - 1];
709 let s = last.value.raw.to_string();
710 if s.is_empty() { None } else { Some(s) }
711 } else {
712 None
713 };
714 self.dynamic_import_patterns.push(DynamicImportPattern {
715 prefix,
716 suffix,
717 span: expr.span,
718 });
719 }
720 }
721 Expression::TemplateLiteral(tpl)
722 if !tpl.quasis.is_empty() && tpl.expressions.is_empty() =>
723 {
724 let value = tpl.quasis[0].value.raw.to_string();
726 if !value.is_empty() {
727 self.dynamic_imports.push(DynamicImportInfo {
728 source: value,
729 span: expr.span,
730 });
731 }
732 }
733 Expression::BinaryExpression(bin)
734 if bin.operator == oxc_ast::ast::BinaryOperator::Addition =>
735 {
736 if let Some((prefix, suffix)) = extract_concat_parts(bin)
737 && (prefix.starts_with("./") || prefix.starts_with("../"))
738 {
739 self.dynamic_import_patterns.push(DynamicImportPattern {
740 prefix,
741 suffix,
742 span: expr.span,
743 });
744 }
745 }
746 _ => {}
747 }
748
749 walk::walk_import_expression(self, expr);
750 }
751
752 fn visit_variable_declaration(&mut self, decl: &VariableDeclaration<'a>) {
753 for declarator in &decl.declarations {
754 let Some(Expression::CallExpression(call)) = &declarator.init else {
755 continue;
756 };
757 let Expression::Identifier(callee) = &call.callee else {
758 continue;
759 };
760 if callee.name != "require" {
761 continue;
762 }
763 let Some(Argument::StringLiteral(lit)) = call.arguments.first() else {
764 continue;
765 };
766 let source = lit.value.to_string();
767
768 match &declarator.id {
769 BindingPattern::ObjectPattern(obj_pat) => {
770 if obj_pat.rest.is_some() {
771 self.require_calls.push(RequireCallInfo {
772 source,
773 span: call.span,
774 destructured_names: Vec::new(),
775 local_name: None,
776 });
777 } else {
778 let names: Vec<String> = obj_pat
779 .properties
780 .iter()
781 .filter_map(|prop| prop.key.static_name().map(|n| n.to_string()))
782 .collect();
783 self.require_calls.push(RequireCallInfo {
784 source,
785 span: call.span,
786 destructured_names: names,
787 local_name: None,
788 });
789 }
790 self.handled_require_spans.push(call.span);
791 }
792 BindingPattern::BindingIdentifier(id) => {
793 self.require_calls.push(RequireCallInfo {
795 source,
796 span: call.span,
797 destructured_names: Vec::new(),
798 local_name: Some(id.name.to_string()),
799 });
800 self.handled_require_spans.push(call.span);
801 }
802 _ => {}
803 }
804 }
805 walk::walk_variable_declaration(self, decl);
806 }
807
808 fn visit_call_expression(&mut self, expr: &CallExpression<'a>) {
809 if let Expression::Identifier(ident) = &expr.callee
811 && ident.name == "require"
812 && let Some(Argument::StringLiteral(lit)) = expr.arguments.first()
813 && !self.handled_require_spans.contains(&expr.span)
814 {
815 self.require_calls.push(RequireCallInfo {
816 source: lit.value.to_string(),
817 span: expr.span,
818 destructured_names: Vec::new(),
819 local_name: None,
820 });
821 }
822
823 if let Expression::StaticMemberExpression(member) = &expr.callee
825 && let Expression::Identifier(obj) = &member.object
826 && obj.name == "Object"
827 && matches!(member.property.name.as_str(), "values" | "keys" | "entries")
828 && let Some(Argument::Identifier(arg_ident)) = expr.arguments.first()
829 {
830 self.whole_object_uses.push(arg_ident.name.to_string());
831 }
832
833 if let Expression::StaticMemberExpression(member) = &expr.callee
835 && member.property.name == "glob"
836 && matches!(member.object, Expression::MetaProperty(_))
837 && let Some(first_arg) = expr.arguments.first()
838 {
839 match first_arg {
840 Argument::StringLiteral(lit) => {
841 let s = lit.value.to_string();
842 if s.starts_with("./") || s.starts_with("../") {
843 self.dynamic_import_patterns.push(DynamicImportPattern {
844 prefix: s,
845 suffix: None,
846 span: expr.span,
847 });
848 }
849 }
850 Argument::ArrayExpression(arr) => {
851 for elem in &arr.elements {
852 if let ArrayExpressionElement::StringLiteral(lit) = elem {
853 let s = lit.value.to_string();
854 if s.starts_with("./") || s.starts_with("../") {
855 self.dynamic_import_patterns.push(DynamicImportPattern {
856 prefix: s,
857 suffix: None,
858 span: expr.span,
859 });
860 }
861 }
862 }
863 }
864 _ => {}
865 }
866 }
867
868 if let Expression::StaticMemberExpression(member) = &expr.callee
870 && member.property.name == "context"
871 && let Expression::Identifier(obj) = &member.object
872 && obj.name == "require"
873 && let Some(Argument::StringLiteral(dir_lit)) = expr.arguments.first()
874 {
875 let dir = dir_lit.value.to_string();
876 if dir.starts_with("./") || dir.starts_with("../") {
877 let recursive = expr
878 .arguments
879 .get(1)
880 .is_some_and(|arg| matches!(arg, Argument::BooleanLiteral(b) if b.value));
881 let prefix = if recursive {
882 format!("{dir}/**/")
883 } else {
884 format!("{dir}/")
885 };
886 self.dynamic_import_patterns.push(DynamicImportPattern {
887 prefix,
888 suffix: None,
889 span: expr.span,
890 });
891 }
892 }
893
894 walk::walk_call_expression(self, expr);
895 }
896
897 fn visit_new_expression(&mut self, expr: &oxc_ast::ast::NewExpression<'a>) {
898 if let Expression::Identifier(callee) = &expr.callee
902 && callee.name == "URL"
903 && expr.arguments.len() == 2
904 && let Some(Argument::StringLiteral(path_lit)) = expr.arguments.first()
905 && is_meta_url_arg(&expr.arguments[1])
906 && (path_lit.value.starts_with("./") || path_lit.value.starts_with("../"))
907 {
908 self.dynamic_imports.push(DynamicImportInfo {
909 source: path_lit.value.to_string(),
910 span: expr.span,
911 });
912 }
913
914 walk::walk_new_expression(self, expr);
915 }
916
917 fn visit_assignment_expression(&mut self, expr: &AssignmentExpression<'a>) {
918 if let AssignmentTarget::StaticMemberExpression(member) = &expr.left {
920 if let Expression::Identifier(obj) = &member.object {
921 if obj.name == "module" && member.property.name == "exports" {
922 self.has_cjs_exports = true;
923 if let Expression::ObjectExpression(obj_expr) = &expr.right {
925 for prop in &obj_expr.properties {
926 if let oxc_ast::ast::ObjectPropertyKind::ObjectProperty(p) = prop
927 && let Some(name) = p.key.static_name()
928 {
929 self.exports.push(ExportInfo {
930 name: ExportName::Named(name.to_string()),
931 local_name: None,
932 is_type_only: false,
933 span: p.span,
934 members: vec![],
935 });
936 }
937 }
938 }
939 }
940 if obj.name == "exports" {
941 self.has_cjs_exports = true;
942 self.exports.push(ExportInfo {
943 name: ExportName::Named(member.property.name.to_string()),
944 local_name: None,
945 is_type_only: false,
946 span: expr.span,
947 members: vec![],
948 });
949 }
950 }
951 if matches!(member.object, Expression::ThisExpression(_)) {
954 self.member_accesses.push(MemberAccess {
955 object: "this".to_string(),
956 member: member.property.name.to_string(),
957 });
958 }
959 }
960 walk::walk_assignment_expression(self, expr);
961 }
962
963 fn visit_static_member_expression(&mut self, expr: &StaticMemberExpression<'a>) {
964 if let Expression::Identifier(obj) = &expr.object {
966 self.member_accesses.push(MemberAccess {
967 object: obj.name.to_string(),
968 member: expr.property.name.to_string(),
969 });
970 }
971 if matches!(expr.object, Expression::ThisExpression(_)) {
973 self.member_accesses.push(MemberAccess {
974 object: "this".to_string(),
975 member: expr.property.name.to_string(),
976 });
977 }
978 walk::walk_static_member_expression(self, expr);
979 }
980
981 fn visit_computed_member_expression(&mut self, expr: &ComputedMemberExpression<'a>) {
982 if let Expression::Identifier(obj) = &expr.object {
983 if let Expression::StringLiteral(lit) = &expr.expression {
984 self.member_accesses.push(MemberAccess {
986 object: obj.name.to_string(),
987 member: lit.value.to_string(),
988 });
989 } else {
990 self.whole_object_uses.push(obj.name.to_string());
992 }
993 }
994 walk::walk_computed_member_expression(self, expr);
995 }
996
997 fn visit_for_in_statement(&mut self, stmt: &ForInStatement<'a>) {
998 if let Expression::Identifier(ident) = &stmt.right {
999 self.whole_object_uses.push(ident.name.to_string());
1000 }
1001 walk::walk_for_in_statement(self, stmt);
1002 }
1003
1004 fn visit_spread_element(&mut self, elem: &SpreadElement<'a>) {
1005 if let Expression::Identifier(ident) = &elem.argument {
1006 self.whole_object_uses.push(ident.name.to_string());
1007 }
1008 walk::walk_spread_element(self, elem);
1009 }
1010}
1011
1012fn extract_concat_parts(expr: &BinaryExpression<'_>) -> Option<(String, Option<String>)> {
1014 let prefix = extract_leading_string(&expr.left)?;
1015 let suffix = extract_trailing_string(&expr.right);
1016 Some((prefix, suffix))
1017}
1018
1019fn extract_leading_string(expr: &Expression<'_>) -> Option<String> {
1020 match expr {
1021 Expression::StringLiteral(lit) => Some(lit.value.to_string()),
1022 Expression::BinaryExpression(bin)
1023 if bin.operator == oxc_ast::ast::BinaryOperator::Addition =>
1024 {
1025 extract_leading_string(&bin.left)
1026 }
1027 _ => None,
1028 }
1029}
1030
1031fn extract_trailing_string(expr: &Expression<'_>) -> Option<String> {
1032 match expr {
1033 Expression::StringLiteral(lit) => {
1034 let s = lit.value.to_string();
1035 if s.is_empty() { None } else { Some(s) }
1036 }
1037 _ => None,
1038 }
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043 use super::*;
1044
1045 fn parse_source(source: &str) -> ModuleInfo {
1046 parse_source_to_module(FileId(0), Path::new("test.ts"), source, 0)
1047 }
1048
1049 #[test]
1050 fn extracts_named_exports() {
1051 let info = parse_source("export const foo = 1; export function bar() {}");
1052 assert_eq!(info.exports.len(), 2);
1053 assert_eq!(info.exports[0].name, ExportName::Named("foo".to_string()));
1054 assert_eq!(info.exports[1].name, ExportName::Named("bar".to_string()));
1055 }
1056
1057 #[test]
1058 fn extracts_default_export() {
1059 let info = parse_source("export default function main() {}");
1060 assert_eq!(info.exports.len(), 1);
1061 assert_eq!(info.exports[0].name, ExportName::Default);
1062 }
1063
1064 #[test]
1065 fn extracts_named_imports() {
1066 let info = parse_source("import { foo, bar } from './utils';");
1067 assert_eq!(info.imports.len(), 2);
1068 assert_eq!(
1069 info.imports[0].imported_name,
1070 ImportedName::Named("foo".to_string())
1071 );
1072 assert_eq!(info.imports[0].source, "./utils");
1073 }
1074
1075 #[test]
1076 fn extracts_namespace_import() {
1077 let info = parse_source("import * as utils from './utils';");
1078 assert_eq!(info.imports.len(), 1);
1079 assert_eq!(info.imports[0].imported_name, ImportedName::Namespace);
1080 }
1081
1082 #[test]
1083 fn extracts_side_effect_import() {
1084 let info = parse_source("import './styles.css';");
1085 assert_eq!(info.imports.len(), 1);
1086 assert_eq!(info.imports[0].imported_name, ImportedName::SideEffect);
1087 }
1088
1089 #[test]
1090 fn extracts_re_exports() {
1091 let info = parse_source("export { foo, bar as baz } from './module';");
1092 assert_eq!(info.re_exports.len(), 2);
1093 assert_eq!(info.re_exports[0].imported_name, "foo");
1094 assert_eq!(info.re_exports[0].exported_name, "foo");
1095 assert_eq!(info.re_exports[1].imported_name, "bar");
1096 assert_eq!(info.re_exports[1].exported_name, "baz");
1097 }
1098
1099 #[test]
1100 fn extracts_star_re_export() {
1101 let info = parse_source("export * from './module';");
1102 assert_eq!(info.re_exports.len(), 1);
1103 assert_eq!(info.re_exports[0].imported_name, "*");
1104 assert_eq!(info.re_exports[0].exported_name, "*");
1105 }
1106
1107 #[test]
1108 fn extracts_dynamic_import() {
1109 let info = parse_source("const mod = import('./lazy');");
1110 assert_eq!(info.dynamic_imports.len(), 1);
1111 assert_eq!(info.dynamic_imports[0].source, "./lazy");
1112 }
1113
1114 #[test]
1115 fn extracts_require_call() {
1116 let info = parse_source("const fs = require('fs');");
1117 assert_eq!(info.require_calls.len(), 1);
1118 assert_eq!(info.require_calls[0].source, "fs");
1119 }
1120
1121 #[test]
1122 fn extracts_type_exports() {
1123 let info = parse_source("export type Foo = string; export interface Bar { x: number; }");
1124 assert_eq!(info.exports.len(), 2);
1125 assert!(info.exports[0].is_type_only);
1126 assert!(info.exports[1].is_type_only);
1127 }
1128
1129 #[test]
1130 fn extracts_type_only_imports() {
1131 let info = parse_source("import type { Foo } from './types';");
1132 assert_eq!(info.imports.len(), 1);
1133 assert!(info.imports[0].is_type_only);
1134 }
1135
1136 #[test]
1137 fn detects_cjs_module_exports() {
1138 let info = parse_source("module.exports = { foo: 1 };");
1139 assert!(info.has_cjs_exports);
1140 }
1141
1142 #[test]
1143 fn detects_cjs_exports_property() {
1144 let info = parse_source("exports.foo = 42;");
1145 assert!(info.has_cjs_exports);
1146 assert_eq!(info.exports.len(), 1);
1147 assert_eq!(info.exports[0].name, ExportName::Named("foo".to_string()));
1148 }
1149
1150 #[test]
1151 fn extracts_static_member_accesses() {
1152 let info = parse_source(
1153 "import { Status, MyClass } from './types';\nconsole.log(Status.Active);\nMyClass.create();",
1154 );
1155 assert!(info.member_accesses.len() >= 2);
1157 let has_status_active = info
1158 .member_accesses
1159 .iter()
1160 .any(|a| a.object == "Status" && a.member == "Active");
1161 let has_myclass_create = info
1162 .member_accesses
1163 .iter()
1164 .any(|a| a.object == "MyClass" && a.member == "create");
1165 assert!(has_status_active, "Should capture Status.Active");
1166 assert!(has_myclass_create, "Should capture MyClass.create");
1167 }
1168
1169 #[test]
1170 fn extracts_default_import() {
1171 let info = parse_source("import React from 'react';");
1172 assert_eq!(info.imports.len(), 1);
1173 assert_eq!(info.imports[0].imported_name, ImportedName::Default);
1174 assert_eq!(info.imports[0].local_name, "React");
1175 assert_eq!(info.imports[0].source, "react");
1176 }
1177
1178 #[test]
1179 fn extracts_mixed_import_default_and_named() {
1180 let info = parse_source("import React, { useState, useEffect } from 'react';");
1181 assert_eq!(info.imports.len(), 3);
1182 assert_eq!(info.imports[0].imported_name, ImportedName::Default);
1184 assert_eq!(info.imports[0].local_name, "React");
1185 assert_eq!(
1186 info.imports[1].imported_name,
1187 ImportedName::Named("useState".to_string())
1188 );
1189 assert_eq!(
1190 info.imports[2].imported_name,
1191 ImportedName::Named("useEffect".to_string())
1192 );
1193 }
1194
1195 #[test]
1196 fn extracts_import_with_alias() {
1197 let info = parse_source("import { foo as bar } from './utils';");
1198 assert_eq!(info.imports.len(), 1);
1199 assert_eq!(
1200 info.imports[0].imported_name,
1201 ImportedName::Named("foo".to_string())
1202 );
1203 assert_eq!(info.imports[0].local_name, "bar");
1204 }
1205
1206 #[test]
1207 fn extracts_export_specifier_list() {
1208 let info = parse_source("const foo = 1; const bar = 2; export { foo, bar };");
1209 assert_eq!(info.exports.len(), 2);
1210 assert_eq!(info.exports[0].name, ExportName::Named("foo".to_string()));
1211 assert_eq!(info.exports[1].name, ExportName::Named("bar".to_string()));
1212 }
1213
1214 #[test]
1215 fn extracts_export_with_alias() {
1216 let info = parse_source("const foo = 1; export { foo as myFoo };");
1217 assert_eq!(info.exports.len(), 1);
1218 assert_eq!(info.exports[0].name, ExportName::Named("myFoo".to_string()));
1219 }
1220
1221 #[test]
1222 fn extracts_star_re_export_with_alias() {
1223 let info = parse_source("export * as utils from './utils';");
1224 assert_eq!(info.re_exports.len(), 1);
1225 assert_eq!(info.re_exports[0].imported_name, "*");
1226 assert_eq!(info.re_exports[0].exported_name, "utils");
1227 }
1228
1229 #[test]
1230 fn extracts_export_class_declaration() {
1231 let info = parse_source("export class MyService { name: string = ''; }");
1232 assert_eq!(info.exports.len(), 1);
1233 assert_eq!(
1234 info.exports[0].name,
1235 ExportName::Named("MyService".to_string())
1236 );
1237 }
1238
1239 #[test]
1240 fn class_constructor_is_excluded() {
1241 let info = parse_source("export class Foo { constructor() {} greet() {} }");
1242 assert_eq!(info.exports.len(), 1);
1243 let members: Vec<&str> = info.exports[0]
1245 .members
1246 .iter()
1247 .map(|m| m.name.as_str())
1248 .collect();
1249 assert!(
1250 !members.contains(&"constructor"),
1251 "constructor should be excluded from members"
1252 );
1253 assert!(members.contains(&"greet"), "greet should be included");
1254 }
1255
1256 #[test]
1257 fn extracts_ts_enum_declaration() {
1258 let info = parse_source("export enum Direction { Up, Down, Left, Right }");
1259 assert_eq!(info.exports.len(), 1);
1260 assert_eq!(
1261 info.exports[0].name,
1262 ExportName::Named("Direction".to_string())
1263 );
1264 assert_eq!(info.exports[0].members.len(), 4);
1265 assert_eq!(info.exports[0].members[0].kind, MemberKind::EnumMember);
1266 }
1267
1268 #[test]
1269 fn extracts_ts_module_declaration() {
1270 let info = parse_source("export declare module 'my-module' {}");
1271 assert_eq!(info.exports.len(), 1);
1272 assert!(info.exports[0].is_type_only);
1273 }
1274
1275 #[test]
1276 fn extracts_type_only_named_import() {
1277 let info = parse_source("import { type Foo, Bar } from './types';");
1278 assert_eq!(info.imports.len(), 2);
1279 assert!(info.imports[0].is_type_only);
1280 assert!(!info.imports[1].is_type_only);
1281 }
1282
1283 #[test]
1284 fn extracts_type_re_export() {
1285 let info = parse_source("export type { Foo } from './types';");
1286 assert_eq!(info.re_exports.len(), 1);
1287 assert!(info.re_exports[0].is_type_only);
1288 }
1289
1290 #[test]
1291 fn extracts_destructured_array_export() {
1292 let info = parse_source("export const [first, second] = [1, 2];");
1293 assert_eq!(info.exports.len(), 2);
1294 assert_eq!(info.exports[0].name, ExportName::Named("first".to_string()));
1295 assert_eq!(
1296 info.exports[1].name,
1297 ExportName::Named("second".to_string())
1298 );
1299 }
1300
1301 #[test]
1302 fn extracts_nested_destructured_export() {
1303 let info = parse_source("export const { a, b: { c } } = obj;");
1304 assert_eq!(info.exports.len(), 2);
1305 assert_eq!(info.exports[0].name, ExportName::Named("a".to_string()));
1306 assert_eq!(info.exports[1].name, ExportName::Named("c".to_string()));
1307 }
1308
1309 #[test]
1310 fn extracts_default_export_function_expression() {
1311 let info = parse_source("export default function() { return 42; }");
1312 assert_eq!(info.exports.len(), 1);
1313 assert_eq!(info.exports[0].name, ExportName::Default);
1314 }
1315
1316 #[test]
1317 fn export_name_display() {
1318 assert_eq!(ExportName::Named("foo".to_string()).to_string(), "foo");
1319 assert_eq!(ExportName::Default.to_string(), "default");
1320 }
1321
1322 #[test]
1323 fn no_exports_no_imports() {
1324 let info = parse_source("const x = 1; console.log(x);");
1325 assert!(info.exports.is_empty());
1326 assert!(info.imports.is_empty());
1327 assert!(info.re_exports.is_empty());
1328 assert!(!info.has_cjs_exports);
1329 }
1330
1331 #[test]
1332 fn dynamic_import_non_string_ignored() {
1333 let info = parse_source("const mod = import(variable);");
1334 assert_eq!(info.dynamic_imports.len(), 0);
1336 }
1337
1338 #[test]
1339 fn multiple_require_calls() {
1340 let info =
1341 parse_source("const a = require('a'); const b = require('b'); const c = require('c');");
1342 assert_eq!(info.require_calls.len(), 3);
1343 }
1344
1345 #[test]
1346 fn extracts_ts_interface() {
1347 let info = parse_source("export interface Props { name: string; age: number; }");
1348 assert_eq!(info.exports.len(), 1);
1349 assert_eq!(info.exports[0].name, ExportName::Named("Props".to_string()));
1350 assert!(info.exports[0].is_type_only);
1351 }
1352
1353 #[test]
1354 fn extracts_ts_type_alias() {
1355 let info = parse_source("export type ID = string | number;");
1356 assert_eq!(info.exports.len(), 1);
1357 assert_eq!(info.exports[0].name, ExportName::Named("ID".to_string()));
1358 assert!(info.exports[0].is_type_only);
1359 }
1360
1361 #[test]
1362 fn extracts_member_accesses_inside_exported_functions() {
1363 let info = parse_source(
1364 "import { Color } from './types';\nexport const isRed = (c: Color) => c === Color.Red;",
1365 );
1366 let has_color_red = info
1367 .member_accesses
1368 .iter()
1369 .any(|a| a.object == "Color" && a.member == "Red");
1370 assert!(
1371 has_color_red,
1372 "Should capture Color.Red inside exported function body"
1373 );
1374 }
1375
1376 #[test]
1379 fn detects_object_values_whole_use() {
1380 let info = parse_source("import { Status } from './types';\nObject.values(Status);");
1381 assert!(info.whole_object_uses.contains(&"Status".to_string()));
1382 }
1383
1384 #[test]
1385 fn detects_object_keys_whole_use() {
1386 let info = parse_source("import { Dir } from './types';\nObject.keys(Dir);");
1387 assert!(info.whole_object_uses.contains(&"Dir".to_string()));
1388 }
1389
1390 #[test]
1391 fn detects_object_entries_whole_use() {
1392 let info = parse_source("import { E } from './types';\nObject.entries(E);");
1393 assert!(info.whole_object_uses.contains(&"E".to_string()));
1394 }
1395
1396 #[test]
1397 fn detects_for_in_whole_use() {
1398 let info = parse_source("import { Color } from './types';\nfor (const k in Color) {}");
1399 assert!(info.whole_object_uses.contains(&"Color".to_string()));
1400 }
1401
1402 #[test]
1403 fn detects_spread_whole_use() {
1404 let info = parse_source("import { X } from './types';\nconst y = { ...X };");
1405 assert!(info.whole_object_uses.contains(&"X".to_string()));
1406 }
1407
1408 #[test]
1409 fn computed_member_string_literal_resolves() {
1410 let info = parse_source("import { Status } from './types';\nStatus[\"Active\"];");
1411 let has_access = info
1412 .member_accesses
1413 .iter()
1414 .any(|a| a.object == "Status" && a.member == "Active");
1415 assert!(
1416 has_access,
1417 "Status[\"Active\"] should resolve to a static member access"
1418 );
1419 }
1420
1421 #[test]
1422 fn computed_member_variable_marks_whole_use() {
1423 let info = parse_source("import { Status } from './types';\nconst k = 'foo';\nStatus[k];");
1424 assert!(info.whole_object_uses.contains(&"Status".to_string()));
1425 }
1426
1427 #[test]
1430 fn extracts_template_literal_dynamic_import_pattern() {
1431 let info = parse_source("const m = import(`./locales/${lang}.json`);");
1432 assert_eq!(info.dynamic_import_patterns.len(), 1);
1433 assert_eq!(info.dynamic_import_patterns[0].prefix, "./locales/");
1434 assert_eq!(
1435 info.dynamic_import_patterns[0].suffix,
1436 Some(".json".to_string())
1437 );
1438 }
1439
1440 #[test]
1441 fn extracts_concat_dynamic_import_pattern() {
1442 let info = parse_source("const m = import('./pages/' + name);");
1443 assert_eq!(info.dynamic_import_patterns.len(), 1);
1444 assert_eq!(info.dynamic_import_patterns[0].prefix, "./pages/");
1445 assert!(info.dynamic_import_patterns[0].suffix.is_none());
1446 }
1447
1448 #[test]
1449 fn extracts_concat_with_suffix() {
1450 let info = parse_source("const m = import('./pages/' + name + '.tsx');");
1451 assert_eq!(info.dynamic_import_patterns.len(), 1);
1452 assert_eq!(info.dynamic_import_patterns[0].prefix, "./pages/");
1453 assert_eq!(
1454 info.dynamic_import_patterns[0].suffix,
1455 Some(".tsx".to_string())
1456 );
1457 }
1458
1459 #[test]
1460 fn no_substitution_template_treated_as_exact() {
1461 let info = parse_source("const m = import(`./exact-module`);");
1462 assert_eq!(info.dynamic_imports.len(), 1);
1463 assert_eq!(info.dynamic_imports[0].source, "./exact-module");
1464 assert!(info.dynamic_import_patterns.is_empty());
1465 }
1466
1467 #[test]
1468 fn fully_dynamic_import_still_ignored() {
1469 let info = parse_source("const m = import(variable);");
1470 assert!(info.dynamic_imports.is_empty());
1471 assert!(info.dynamic_import_patterns.is_empty());
1472 }
1473
1474 #[test]
1475 fn non_relative_template_ignored() {
1476 let info = parse_source("const m = import(`lodash/${fn}`);");
1477 assert!(info.dynamic_import_patterns.is_empty());
1478 }
1479
1480 #[test]
1481 fn multi_expression_template_uses_globstar() {
1482 let info = parse_source("const m = import(`./plugins/${cat}/${name}.js`);");
1484 assert_eq!(info.dynamic_import_patterns.len(), 1);
1485 assert_eq!(info.dynamic_import_patterns[0].prefix, "./plugins/**/");
1486 assert_eq!(
1487 info.dynamic_import_patterns[0].suffix,
1488 Some(".js".to_string())
1489 );
1490 }
1491
1492 fn parse_sfc(source: &str, filename: &str) -> ModuleInfo {
1495 parse_source_to_module(FileId(0), Path::new(filename), source, 0)
1496 }
1497
1498 #[test]
1499 fn extracts_vue_script_imports() {
1500 let info = parse_sfc(
1501 r#"
1502<script lang="ts">
1503import { ref } from 'vue';
1504import { helper } from './utils';
1505export default {};
1506</script>
1507<template><div></div></template>
1508"#,
1509 "App.vue",
1510 );
1511 assert_eq!(info.imports.len(), 2);
1512 assert!(info.imports.iter().any(|i| i.source == "vue"));
1513 assert!(info.imports.iter().any(|i| i.source == "./utils"));
1514 }
1515
1516 #[test]
1517 fn extracts_vue_script_setup_imports() {
1518 let info = parse_sfc(
1519 r#"
1520<script setup lang="ts">
1521import { ref } from 'vue';
1522const count = ref(0);
1523</script>
1524"#,
1525 "Comp.vue",
1526 );
1527 assert_eq!(info.imports.len(), 1);
1528 assert_eq!(info.imports[0].source, "vue");
1529 }
1530
1531 #[test]
1532 fn extracts_vue_both_scripts() {
1533 let info = parse_sfc(
1534 r#"
1535<script lang="ts">
1536import { defineComponent } from 'vue';
1537export default defineComponent({});
1538</script>
1539<script setup lang="ts">
1540import { ref } from 'vue';
1541const count = ref(0);
1542</script>
1543"#,
1544 "Dual.vue",
1545 );
1546 assert!(info.imports.len() >= 2);
1547 }
1548
1549 #[test]
1550 fn extracts_svelte_script_imports() {
1551 let info = parse_sfc(
1552 r#"
1553<script lang="ts">
1554import { onMount } from 'svelte';
1555import { helper } from './utils';
1556</script>
1557<p>Hello</p>
1558"#,
1559 "App.svelte",
1560 );
1561 assert_eq!(info.imports.len(), 2);
1562 assert!(info.imports.iter().any(|i| i.source == "svelte"));
1563 assert!(info.imports.iter().any(|i| i.source == "./utils"));
1564 }
1565
1566 #[test]
1567 fn vue_no_script_returns_empty() {
1568 let info = parse_sfc(
1569 "<template><div></div></template><style>div {}</style>",
1570 "NoScript.vue",
1571 );
1572 assert!(info.imports.is_empty());
1573 assert!(info.exports.is_empty());
1574 }
1575
1576 #[test]
1577 fn vue_js_default_lang() {
1578 let info = parse_sfc(
1579 r#"
1580<script>
1581import { createApp } from 'vue';
1582export default {};
1583</script>
1584"#,
1585 "JsVue.vue",
1586 );
1587 assert_eq!(info.imports.len(), 1);
1588 }
1589
1590 #[test]
1593 fn extracts_import_meta_glob_pattern() {
1594 let info = parse_source("const mods = import.meta.glob('./components/*.tsx');");
1595 assert_eq!(info.dynamic_import_patterns.len(), 1);
1596 assert_eq!(info.dynamic_import_patterns[0].prefix, "./components/*.tsx");
1597 }
1598
1599 #[test]
1600 fn extracts_import_meta_glob_array() {
1601 let info =
1602 parse_source("const mods = import.meta.glob(['./pages/*.ts', './layouts/*.ts']);");
1603 assert_eq!(info.dynamic_import_patterns.len(), 2);
1604 assert_eq!(info.dynamic_import_patterns[0].prefix, "./pages/*.ts");
1605 assert_eq!(info.dynamic_import_patterns[1].prefix, "./layouts/*.ts");
1606 }
1607
1608 #[test]
1609 fn extracts_require_context_pattern() {
1610 let info = parse_source("const ctx = require.context('./icons', false);");
1611 assert_eq!(info.dynamic_import_patterns.len(), 1);
1612 assert_eq!(info.dynamic_import_patterns[0].prefix, "./icons/");
1613 }
1614
1615 #[test]
1616 fn extracts_require_context_recursive() {
1617 let info = parse_source("const ctx = require.context('./icons', true);");
1618 assert_eq!(info.dynamic_import_patterns.len(), 1);
1619 assert_eq!(info.dynamic_import_patterns[0].prefix, "./icons/**/");
1620 }
1621}