1use crate::external_packages::ResolvedPackage;
7use crate::{Export, Import, Symbol, SymbolKind, Visibility};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use tree_sitter::Node;
11
12pub const JS_CONTAINER_KINDS: &[&str] = &["class_declaration", "class"];
17pub const TS_CONTAINER_KINDS: &[&str] = &["class_declaration", "class", "interface_declaration"];
18
19pub const JS_FUNCTION_KINDS: &[&str] = &[
20 "function_declaration",
21 "method_definition",
22 "generator_function_declaration",
23];
24pub const TS_FUNCTION_KINDS: &[&str] = &[
25 "function_declaration",
26 "method_definition",
27 "method_signature", ];
29
30pub const JS_TYPE_KINDS: &[&str] = &["class_declaration"];
31pub const TS_TYPE_KINDS: &[&str] = &[
32 "class_declaration",
33 "interface_declaration",
34 "type_alias_declaration",
35 "enum_declaration",
36];
37
38pub const IMPORT_KINDS: &[&str] = &["import_statement"];
39pub const PUBLIC_SYMBOL_KINDS: &[&str] = &["export_statement"];
40
41pub const SCOPE_CREATING_KINDS: &[&str] = &[
42 "for_statement",
43 "for_in_statement",
44 "while_statement",
45 "do_statement",
46 "try_statement",
47 "catch_clause",
48 "switch_statement",
49 "arrow_function",
50];
51
52pub const CONTROL_FLOW_KINDS: &[&str] = &[
53 "if_statement",
54 "for_statement",
55 "for_in_statement",
56 "while_statement",
57 "do_statement",
58 "switch_statement",
59 "try_statement",
60 "return_statement",
61 "break_statement",
62 "continue_statement",
63 "throw_statement",
64];
65
66pub const COMPLEXITY_NODES: &[&str] = &[
67 "if_statement",
68 "for_statement",
69 "for_in_statement",
70 "while_statement",
71 "do_statement",
72 "switch_case",
73 "catch_clause",
74 "ternary_expression",
75 "binary_expression",
76];
77
78pub const NESTING_NODES: &[&str] = &[
79 "if_statement",
80 "for_statement",
81 "for_in_statement",
82 "while_statement",
83 "do_statement",
84 "switch_statement",
85 "try_statement",
86 "function_declaration",
87 "method_definition",
88 "class_declaration",
89];
90
91pub fn extract_function(node: &Node, content: &str, in_container: bool, name: &str) -> Symbol {
97 let params = node
98 .child_by_field_name("parameters")
99 .map(|p| content[p.byte_range()].to_string())
100 .unwrap_or_else(|| "()".to_string());
101
102 let signature = if node.kind() == "method_definition" {
103 format!("{}{}", name, params)
104 } else {
105 format!("function {}{}", name, params)
106 };
107
108 let is_override = {
110 let mut cursor = node.walk();
111 let children: Vec<_> = node.children(&mut cursor).collect();
112 children
113 .iter()
114 .any(|child| child.kind() == "override_modifier")
115 };
116
117 Symbol {
118 name: name.to_string(),
119 kind: if in_container {
120 SymbolKind::Method
121 } else {
122 SymbolKind::Function
123 },
124 signature,
125 docstring: None,
126 attributes: Vec::new(),
127 start_line: node.start_position().row + 1,
128 end_line: node.end_position().row + 1,
129 visibility: Visibility::Public,
130 children: Vec::new(),
131 is_interface_impl: is_override,
132 implements: Vec::new(),
133 }
134}
135
136pub fn extract_container(node: &Node, content: &str, name: &str) -> Symbol {
138 let (kind, keyword) = if node.kind() == "interface_declaration" {
139 (SymbolKind::Interface, "interface")
140 } else {
141 (SymbolKind::Class, "class")
142 };
143
144 let mut implements = Vec::new();
146 for i in 0..node.child_count() as u32 {
148 if let Some(heritage) = node.child(i) {
149 if heritage.kind() == "class_heritage" {
150 for j in 0..heritage.child_count() as u32 {
152 if let Some(clause) = heritage.child(j) {
153 if clause.kind() == "extends_clause" || clause.kind() == "implements_clause"
154 {
155 for k in 0..clause.child_count() as u32 {
157 if let Some(type_node) = clause.child(k) {
158 if type_node.kind() == "type_identifier"
159 || type_node.kind() == "identifier"
160 {
161 implements
162 .push(content[type_node.byte_range()].to_string());
163 }
164 }
165 }
166 }
167 }
168 }
169 }
170 }
171 }
172
173 Symbol {
174 name: name.to_string(),
175 kind,
176 signature: format!("{} {}", keyword, name),
177 docstring: None,
178 attributes: Vec::new(),
179 start_line: node.start_position().row + 1,
180 end_line: node.end_position().row + 1,
181 visibility: Visibility::Public,
182 children: Vec::new(),
183 is_interface_impl: false,
184 implements,
185 }
186}
187
188pub fn extract_type(node: &Node, name: &str) -> Option<Symbol> {
190 let (kind, keyword) = match node.kind() {
191 "interface_declaration" => (SymbolKind::Interface, "interface"),
192 "type_alias_declaration" => (SymbolKind::Type, "type"),
193 "enum_declaration" => (SymbolKind::Enum, "enum"),
194 "class_declaration" => (SymbolKind::Class, "class"),
195 _ => return None,
196 };
197
198 Some(Symbol {
199 name: name.to_string(),
200 kind,
201 signature: format!("{} {}", keyword, name),
202 docstring: None,
203 attributes: Vec::new(),
204 start_line: node.start_position().row + 1,
205 end_line: node.end_position().row + 1,
206 visibility: Visibility::Public,
207 children: Vec::new(),
208 is_interface_impl: false,
209 implements: Vec::new(),
210 })
211}
212
213pub fn extract_imports(node: &Node, content: &str) -> Vec<Import> {
219 if node.kind() != "import_statement" {
220 return Vec::new();
221 }
222
223 let line = node.start_position().row + 1;
224 let mut module = String::new();
225 let mut names = Vec::new();
226
227 let mut cursor = node.walk();
228 for child in node.children(&mut cursor) {
229 match child.kind() {
230 "string" | "string_fragment" => {
231 let text = &content[child.byte_range()];
232 module = text.trim_matches(|c| c == '"' || c == '\'').to_string();
233 }
234 "import_clause" => {
235 collect_import_names(&child, content, &mut names);
236 }
237 _ => {}
238 }
239 }
240
241 if module.is_empty() {
242 return Vec::new();
243 }
244
245 vec![Import {
246 module: module.clone(),
247 names,
248 alias: None,
249 is_wildcard: false,
250 is_relative: module.starts_with('.'),
251 line,
252 }]
253}
254
255pub fn format_import(import: &Import, names: Option<&[&str]>) -> String {
257 let names_to_use: Vec<&str> = names
258 .map(|n| n.to_vec())
259 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
260
261 if import.is_wildcard {
262 format!("import * from '{}';", import.module)
263 } else if names_to_use.is_empty() {
264 format!("import '{}';", import.module)
265 } else if names_to_use.len() == 1 {
266 format!("import {{ {} }} from '{}';", names_to_use[0], import.module)
267 } else {
268 format!(
269 "import {{ {} }} from '{}';",
270 names_to_use.join(", "),
271 import.module
272 )
273 }
274}
275
276fn collect_import_names(import_clause: &Node, content: &str, names: &mut Vec<String>) {
277 let mut cursor = import_clause.walk();
278 for child in import_clause.children(&mut cursor) {
279 match child.kind() {
280 "identifier" => {
281 names.push(content[child.byte_range()].to_string());
283 }
284 "named_imports" => {
285 let mut inner_cursor = child.walk();
287 for inner in child.children(&mut inner_cursor) {
288 if inner.kind() == "import_specifier" {
289 if let Some(name_node) = inner.child_by_field_name("name") {
290 names.push(content[name_node.byte_range()].to_string());
291 }
292 }
293 }
294 }
295 "namespace_import" => {
296 if let Some(name_node) = child.child_by_field_name("name") {
298 names.push(format!("* as {}", &content[name_node.byte_range()]));
299 }
300 }
301 _ => {}
302 }
303 }
304}
305
306pub fn extract_public_symbols(node: &Node, content: &str) -> Vec<Export> {
308 if node.kind() != "export_statement" {
309 return Vec::new();
310 }
311
312 let line = node.start_position().row + 1;
313 let mut exports = Vec::new();
314
315 let mut cursor = node.walk();
316 for child in node.children(&mut cursor) {
317 match child.kind() {
318 "function_declaration" | "generator_function_declaration" => {
319 if let Some(name_node) = child.child_by_field_name("name") {
320 exports.push(Export {
321 name: content[name_node.byte_range()].to_string(),
322 kind: SymbolKind::Function,
323 line,
324 });
325 }
326 }
327 "class_declaration" => {
328 if let Some(name_node) = child.child_by_field_name("name") {
329 exports.push(Export {
330 name: content[name_node.byte_range()].to_string(),
331 kind: SymbolKind::Class,
332 line,
333 });
334 }
335 }
336 "lexical_declaration" => {
337 let mut decl_cursor = child.walk();
339 for decl_child in child.children(&mut decl_cursor) {
340 if decl_child.kind() == "variable_declarator" {
341 if let Some(name_node) = decl_child.child_by_field_name("name") {
342 exports.push(Export {
343 name: content[name_node.byte_range()].to_string(),
344 kind: SymbolKind::Variable,
345 line,
346 });
347 }
348 }
349 }
350 }
351 _ => {}
352 }
353 }
354
355 exports
356}
357
358pub fn resolve_local_import(
364 module: &str,
365 current_file: &Path,
366 extensions: &[&str],
367) -> Option<PathBuf> {
368 if !module.starts_with('.') {
370 return None;
371 }
372
373 let current_dir = current_file.parent()?;
374
375 let target = if module.starts_with("./") {
377 current_dir.join(&module[2..])
378 } else if module.starts_with("../") {
379 current_dir.join(module)
380 } else {
381 return None;
382 };
383
384 if target.exists() && target.is_file() {
386 return Some(target);
387 }
388
389 for ext in extensions {
391 let with_ext = target.with_extension(ext);
392 if with_ext.exists() && with_ext.is_file() {
393 return Some(with_ext);
394 }
395 }
396
397 if target.is_dir() {
399 for ext in extensions {
400 let index = target.join(format!("index.{}", ext));
401 if index.exists() && index.is_file() {
402 return Some(index);
403 }
404 }
405 }
406
407 None
408}
409
410pub fn find_node_modules(start: &Path) -> Option<PathBuf> {
416 let mut current = if start.is_file() {
417 start.parent()?.to_path_buf()
418 } else {
419 start.to_path_buf()
420 };
421
422 loop {
423 let node_modules = current.join("node_modules");
424 if node_modules.is_dir() {
425 return Some(node_modules);
426 }
427
428 if !current.pop() {
429 break;
430 }
431 }
432
433 None
434}
435
436pub fn get_node_version() -> Option<String> {
438 let output = Command::new("node").args(["--version"]).output().ok()?;
439
440 if output.status.success() {
441 let version_str = String::from_utf8_lossy(&output.stdout);
442 let ver = version_str.trim().trim_start_matches('v');
444 let parts: Vec<&str> = ver.split('.').collect();
445 if parts.len() >= 2 {
446 return Some(format!("{}.{}", parts[0], parts[1]));
447 }
448 }
449
450 None
451}
452
453fn resolve_node_import(import_path: &str, node_modules: &Path) -> Option<ResolvedPackage> {
460 let parsed = parse_node_package_name(import_path);
462
463 let pkg_dir = node_modules.join(&parsed.name);
464 if !pkg_dir.is_dir() {
465 return None;
466 }
467
468 if let Some(subpath) = parsed.subpath {
470 let target = pkg_dir.join(subpath);
471 if let Some(resolved) = resolve_node_file_or_dir(&target) {
472 return Some(ResolvedPackage {
473 path: resolved,
474 name: import_path.to_string(),
475 is_namespace: false,
476 });
477 }
478 return None;
479 }
480
481 let pkg_json = pkg_dir.join("package.json");
483 if pkg_json.is_file() {
484 if let Some(entry) = get_package_entry_point(&pkg_dir, &pkg_json) {
485 return Some(ResolvedPackage {
486 path: entry,
487 name: import_path.to_string(),
488 is_namespace: false,
489 });
490 }
491 }
492
493 if let Some(resolved) = resolve_node_file_or_dir(&pkg_dir) {
495 return Some(ResolvedPackage {
496 path: resolved,
497 name: import_path.to_string(),
498 is_namespace: false,
499 });
500 }
501
502 None
503}
504
505struct ParsedPackage<'a> {
507 name: String,
508 subpath: Option<&'a str>,
509}
510
511fn parse_node_package_name(import_path: &str) -> ParsedPackage<'_> {
513 if import_path.starts_with('@') {
514 let parts: Vec<&str> = import_path.splitn(3, '/').collect();
516 if parts.len() >= 2 {
517 let name = format!("{}/{}", parts[0], parts[1]);
518 let subpath = if parts.len() > 2 {
519 Some(parts[2])
520 } else {
521 None
522 };
523 return ParsedPackage { name, subpath };
524 }
525 ParsedPackage {
526 name: import_path.to_string(),
527 subpath: None,
528 }
529 } else {
530 if let Some(idx) = import_path.find('/') {
532 let name = import_path[..idx].to_string();
533 let subpath = Some(&import_path[idx + 1..]);
534 ParsedPackage { name, subpath }
535 } else {
536 ParsedPackage {
537 name: import_path.to_string(),
538 subpath: None,
539 }
540 }
541 }
542}
543
544fn get_package_entry_point(pkg_dir: &Path, pkg_json: &Path) -> Option<PathBuf> {
546 let content = std::fs::read_to_string(pkg_json).ok()?;
547 let json: serde_json::Value = serde_json::from_str(&content).ok()?;
548
549 if let Some(exports) = json.get("exports")
551 && let Some(entry) = exports.as_str()
552 {
553 let path = pkg_dir.join(entry.trim_start_matches("./"));
554 if path.is_file() {
555 return Some(path);
556 }
557 } else if let Some(exports) = json.get("exports")
558 && let Some(obj) = exports.as_object()
559 && let Some(dot) = obj.get(".")
560 && let Some(entry) = extract_export_entry(dot)
561 {
562 let path = pkg_dir.join(entry.trim_start_matches("./"));
563 if path.is_file() {
564 return Some(path);
565 }
566 }
567
568 if let Some(module) = json.get("module").and_then(|v| v.as_str()) {
570 let path = pkg_dir.join(module.trim_start_matches("./"));
571 if path.is_file() {
572 return Some(path);
573 }
574 }
575
576 if let Some(main) = json.get("main").and_then(|v| v.as_str()) {
578 let path = pkg_dir.join(main.trim_start_matches("./"));
579 if let Some(resolved) = resolve_node_file_or_dir(&path) {
580 return Some(resolved);
581 }
582 }
583
584 None
585}
586
587fn extract_export_entry(value: &serde_json::Value) -> Option<&str> {
589 if let Some(s) = value.as_str() {
590 return Some(s);
591 }
592 if let Some(obj) = value.as_object() {
593 for key in &["import", "require", "default"] {
595 if let Some(entry) = obj.get(*key) {
596 if let Some(s) = entry.as_str() {
597 return Some(s);
598 }
599 if let Some(s) = extract_export_entry(entry) {
601 return Some(s);
602 }
603 }
604 }
605 }
606 None
607}
608
609fn resolve_node_file_or_dir(target: &Path) -> Option<PathBuf> {
611 let extensions = ["js", "mjs", "cjs", "ts", "tsx", "jsx"];
612
613 if target.is_file() {
615 return Some(target.to_path_buf());
616 }
617
618 for ext in &extensions {
620 let with_ext = target.with_extension(ext);
621 if with_ext.is_file() {
622 return Some(with_ext);
623 }
624 }
625
626 if target.is_dir() {
628 for ext in &extensions {
629 let index = target.join(format!("index.{}", ext));
630 if index.is_file() {
631 return Some(index);
632 }
633 }
634 }
635
636 None
637}
638
639pub fn resolve_external_import(import_name: &str, project_root: &Path) -> Option<ResolvedPackage> {
641 if import_name.starts_with('.') || import_name.starts_with('/') {
642 return None;
643 }
644
645 let node_modules = find_node_modules(project_root)?;
646 resolve_node_import(import_name, &node_modules)
647}
648
649pub fn get_version() -> Option<String> {
651 get_node_version()
652}
653
654pub fn find_package_cache(project_root: &Path) -> Option<PathBuf> {
656 find_node_modules(project_root)
657}
658
659pub const JS_EXTENSIONS: &[&str] = &["js", "jsx", "mjs", "cjs"];
661pub const TS_EXTENSIONS: &[&str] = &["ts", "tsx", "js", "jsx", "mts", "mjs"];
662
663pub fn get_deno_version() -> Option<String> {
669 let output = Command::new("deno").args(["--version"]).output().ok()?;
670
671 if output.status.success() {
672 let version_str = String::from_utf8_lossy(&output.stdout);
673 for line in version_str.lines() {
674 if line.starts_with("deno ") {
675 let version_part = line.strip_prefix("deno ")?;
676 let parts: Vec<&str> = version_part.split('.').collect();
677 if parts.len() >= 2 {
678 let major = parts[0].trim();
679 let minor = parts[1]
680 .chars()
681 .take_while(|c| c.is_ascii_digit())
682 .collect::<String>();
683 return Some(format!("{}.{}", major, minor));
684 }
685 }
686 }
687 }
688
689 None
690}
691
692pub fn find_deno_cache() -> Option<PathBuf> {
694 if let Ok(deno_dir) = std::env::var("DENO_DIR") {
695 let cache = PathBuf::from(deno_dir);
696 if cache.is_dir() {
697 return Some(cache);
698 }
699 }
700
701 #[cfg(target_os = "macos")]
702 {
703 if let Ok(home) = std::env::var("HOME") {
704 let cache = PathBuf::from(home).join("Library/Caches/deno");
705 if cache.is_dir() {
706 return Some(cache);
707 }
708 }
709 }
710
711 #[cfg(target_os = "linux")]
712 {
713 if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
714 let cache = PathBuf::from(xdg_cache).join("deno");
715 if cache.is_dir() {
716 return Some(cache);
717 }
718 }
719 if let Ok(home) = std::env::var("HOME") {
720 let cache = PathBuf::from(home).join(".cache/deno");
721 if cache.is_dir() {
722 return Some(cache);
723 }
724 }
725 }
726
727 #[cfg(target_os = "windows")]
728 {
729 if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
730 let cache = PathBuf::from(local_app_data).join("deno");
731 if cache.is_dir() {
732 return Some(cache);
733 }
734 }
735 }
736
737 if let Ok(home) = std::env::var("HOME") {
738 for path in &[".cache/deno", "Library/Caches/deno"] {
739 let cache = PathBuf::from(&home).join(path);
740 if cache.is_dir() {
741 return Some(cache);
742 }
743 }
744 }
745
746 None
747}
748
749pub fn resolve_deno_import(import_url: &str, cache: &Path) -> Option<ResolvedPackage> {
751 if let Some(npm_spec) = import_url.strip_prefix("npm:") {
752 return resolve_deno_npm_import(npm_spec, cache);
753 }
754
755 if import_url.starts_with("https://") || import_url.starts_with("http://") {
756 return resolve_deno_url_import(import_url, cache);
757 }
758
759 None
760}
761
762fn resolve_deno_npm_import(npm_spec: &str, cache: &Path) -> Option<ResolvedPackage> {
763 let npm_cache = cache.join("npm").join("registry.npmjs.org");
764 if !npm_cache.is_dir() {
765 return None;
766 }
767
768 let (pkg_name, version_spec) = if npm_spec.starts_with('@') {
769 let parts: Vec<&str> = npm_spec.splitn(3, '/').collect();
770 if parts.len() < 2 {
771 return None;
772 }
773 let scope = parts[0];
774 let name_ver = parts[1];
775 let (name, ver) = if let Some(idx) = name_ver.rfind('@') {
776 (&name_ver[..idx], Some(&name_ver[idx + 1..]))
777 } else {
778 (name_ver, None)
779 };
780 (format!("{}/{}", scope, name), ver)
781 } else if let Some(idx) = npm_spec.rfind('@') {
782 (npm_spec[..idx].to_string(), Some(&npm_spec[idx + 1..]))
783 } else {
784 (npm_spec.to_string(), None)
785 };
786
787 let pkg_path = if pkg_name.starts_with('@') {
788 let parts: Vec<&str> = pkg_name.splitn(2, '/').collect();
789 npm_cache.join(parts[0]).join(parts[1])
790 } else {
791 npm_cache.join(&pkg_name)
792 };
793
794 if !pkg_path.is_dir() {
795 return None;
796 }
797
798 let version_dir = find_best_version_dir(&pkg_path, version_spec)?;
799 let entry = find_package_entry(&version_dir)?;
800
801 Some(ResolvedPackage {
802 path: entry,
803 name: pkg_name,
804 is_namespace: false,
805 })
806}
807
808fn resolve_deno_url_import(url: &str, cache: &Path) -> Option<ResolvedPackage> {
809 let deps_dir = cache.join("deps");
810 let url_parsed = url
811 .strip_prefix("https://")
812 .or_else(|| url.strip_prefix("http://"))?;
813 let scheme = if url.starts_with("https://") {
814 "https"
815 } else {
816 "http"
817 };
818
819 let scheme_dir = deps_dir.join(scheme);
820 if !scheme_dir.is_dir() {
821 return None;
822 }
823
824 let (host, path) = url_parsed.split_once('/')?;
825 let host_dir = scheme_dir.join(host);
826 if !host_dir.is_dir() {
827 return None;
828 }
829
830 if let Ok(entries) = std::fs::read_dir(&host_dir) {
831 for entry in entries.flatten() {
832 let entry_path = entry.path();
833 let name = entry.file_name().to_string_lossy().to_string();
834
835 if name.ends_with(".metadata.json") {
836 continue;
837 }
838
839 let meta_path = host_dir.join(format!("{}.metadata.json", name));
840 if meta_path.is_file() {
841 if let Ok(meta_content) = std::fs::read_to_string(&meta_path) {
842 if meta_content.contains(url) {
843 return Some(ResolvedPackage {
844 path: entry_path,
845 name: format!("{}/{}", host, path),
846 is_namespace: false,
847 });
848 }
849 }
850 }
851 }
852 }
853
854 None
855}
856
857fn find_best_version_dir(pkg_path: &Path, version_spec: Option<&str>) -> Option<PathBuf> {
858 let entries: Vec<_> = std::fs::read_dir(pkg_path).ok()?.flatten().collect();
859
860 if let Some(spec) = version_spec {
861 let exact = pkg_path.join(spec);
862 if exact.is_dir() {
863 return Some(exact);
864 }
865
866 for entry in &entries {
867 let name = entry.file_name().to_string_lossy().to_string();
868 if name.starts_with(spec) && entry.path().is_dir() {
869 return Some(entry.path());
870 }
871 }
872 }
873
874 let mut versions: Vec<_> = entries.into_iter().filter(|e| e.path().is_dir()).collect();
875 versions.sort_by(|a, b| {
876 let a_name = a.file_name().to_string_lossy().to_string();
877 let b_name = b.file_name().to_string_lossy().to_string();
878 deno_version_cmp(&a_name, &b_name)
879 });
880 versions.last().map(|e| e.path())
881}
882
883fn deno_version_cmp(a: &str, b: &str) -> std::cmp::Ordering {
884 let a_parts: Vec<u32> = a.split('.').filter_map(|p| p.parse().ok()).collect();
885 let b_parts: Vec<u32> = b.split('.').filter_map(|p| p.parse().ok()).collect();
886
887 for (ap, bp) in a_parts.iter().zip(b_parts.iter()) {
888 match ap.cmp(bp) {
889 std::cmp::Ordering::Equal => continue,
890 other => return other,
891 }
892 }
893 a_parts.len().cmp(&b_parts.len())
894}
895
896pub fn find_package_entry(dir: &Path) -> Option<PathBuf> {
899 let pkg_json = dir.join("package.json");
900 if pkg_json.is_file()
901 && let Ok(content) = std::fs::read_to_string(&pkg_json)
902 && let Ok(json) = serde_json::from_str::<serde_json::Value>(&content)
903 {
904 for field in &["module", "main"] {
905 if let Some(entry) = json.get(field).and_then(|v| v.as_str()) {
906 let path = dir.join(entry.trim_start_matches("./"));
907 if path.is_file() {
908 return Some(path);
909 }
910 let with_ext = path.with_extension("js");
911 if with_ext.is_file() {
912 return Some(with_ext);
913 }
914 }
915 }
916 }
917
918 for ext in &["js", "mjs", "cjs", "ts"] {
919 let index = dir.join(format!("index.{}", ext));
920 if index.is_file() {
921 return Some(index);
922 }
923 }
924
925 None
926}