1use std::path::{Path, PathBuf};
2
3use super::parse_scripts::extract_script_file_refs;
4use super::walk::SOURCE_EXTENSIONS;
5use fallow_config::{EntryPointRole, PackageJson, ResolvedConfig};
6use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource};
7
8const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
12
13#[derive(Debug, Clone, Default)]
15pub struct CategorizedEntryPoints {
16 pub all: Vec<EntryPoint>,
17 pub runtime: Vec<EntryPoint>,
18 pub test: Vec<EntryPoint>,
19}
20
21impl CategorizedEntryPoints {
22 pub fn push_runtime(&mut self, entry: EntryPoint) {
23 self.runtime.push(entry.clone());
24 self.all.push(entry);
25 }
26
27 pub fn push_test(&mut self, entry: EntryPoint) {
28 self.test.push(entry.clone());
29 self.all.push(entry);
30 }
31
32 pub fn push_support(&mut self, entry: EntryPoint) {
33 self.all.push(entry);
34 }
35
36 pub fn extend_runtime<I>(&mut self, entries: I)
37 where
38 I: IntoIterator<Item = EntryPoint>,
39 {
40 for entry in entries {
41 self.push_runtime(entry);
42 }
43 }
44
45 pub fn extend_test<I>(&mut self, entries: I)
46 where
47 I: IntoIterator<Item = EntryPoint>,
48 {
49 for entry in entries {
50 self.push_test(entry);
51 }
52 }
53
54 pub fn extend_support<I>(&mut self, entries: I)
55 where
56 I: IntoIterator<Item = EntryPoint>,
57 {
58 for entry in entries {
59 self.push_support(entry);
60 }
61 }
62
63 pub fn extend(&mut self, other: Self) {
64 self.all.extend(other.all);
65 self.runtime.extend(other.runtime);
66 self.test.extend(other.test);
67 }
68
69 #[must_use]
70 pub fn dedup(mut self) -> Self {
71 dedup_entry_paths(&mut self.all);
72 dedup_entry_paths(&mut self.runtime);
73 dedup_entry_paths(&mut self.test);
74 self
75 }
76}
77
78fn dedup_entry_paths(entries: &mut Vec<EntryPoint>) {
79 entries.sort_by(|a, b| a.path.cmp(&b.path));
80 entries.dedup_by(|a, b| a.path == b.path);
81}
82
83pub fn resolve_entry_path(
90 base: &Path,
91 entry: &str,
92 canonical_root: &Path,
93 source: EntryPointSource,
94) -> Option<EntryPoint> {
95 if entry.contains('*') {
98 return None;
99 }
100
101 let resolved = base.join(entry);
102 let canonical_resolved = dunce::canonicalize(&resolved).unwrap_or_else(|_| resolved.clone());
104 if !canonical_resolved.starts_with(canonical_root) {
105 tracing::warn!(path = %entry, "Skipping entry point outside project root");
106 return None;
107 }
108
109 if let Some(source_path) = try_output_to_source_path(base, entry) {
114 if let Ok(canonical_source) = dunce::canonicalize(&source_path)
116 && canonical_source.starts_with(canonical_root)
117 {
118 return Some(EntryPoint {
119 path: source_path,
120 source,
121 });
122 }
123 }
124
125 if is_entry_in_output_dir(entry)
133 && let Some(source_path) = try_source_index_fallback(base)
134 && let Ok(canonical_source) = dunce::canonicalize(&source_path)
135 && canonical_source.starts_with(canonical_root)
136 {
137 tracing::info!(
138 entry = %entry,
139 fallback = %source_path.display(),
140 "package.json entry resolves to an ignored output directory; falling back to source index"
141 );
142 return Some(EntryPoint {
143 path: source_path,
144 source,
145 });
146 }
147
148 if resolved.exists() {
149 return Some(EntryPoint {
150 path: resolved,
151 source,
152 });
153 }
154 for ext in SOURCE_EXTENSIONS {
156 let with_ext = resolved.with_extension(ext);
157 if with_ext.exists() {
158 return Some(EntryPoint {
159 path: with_ext,
160 source,
161 });
162 }
163 }
164 None
165}
166
167fn try_output_to_source_path(base: &Path, entry: &str) -> Option<PathBuf> {
179 let entry_path = Path::new(entry);
180 let components: Vec<_> = entry_path.components().collect();
181
182 let output_pos = components.iter().rposition(|c| {
184 if let std::path::Component::Normal(s) = c
185 && let Some(name) = s.to_str()
186 {
187 return OUTPUT_DIRS.contains(&name);
188 }
189 false
190 })?;
191
192 let prefix: PathBuf = components[..output_pos]
194 .iter()
195 .filter(|c| !matches!(c, std::path::Component::CurDir))
196 .collect();
197
198 let suffix: PathBuf = components[output_pos + 1..].iter().collect();
200
201 for ext in SOURCE_EXTENSIONS {
203 let source_candidate = base
204 .join(&prefix)
205 .join("src")
206 .join(suffix.with_extension(ext));
207 if source_candidate.exists() {
208 return Some(source_candidate);
209 }
210 }
211
212 None
213}
214
215const SOURCE_INDEX_FALLBACK_STEMS: &[&str] = &["src/index", "src/main", "index", "main"];
218
219fn is_entry_in_output_dir(entry: &str) -> bool {
224 Path::new(entry).components().any(|c| {
225 matches!(
226 c,
227 std::path::Component::Normal(s)
228 if s.to_str().is_some_and(|name| OUTPUT_DIRS.contains(&name))
229 )
230 })
231}
232
233fn try_source_index_fallback(base: &Path) -> Option<PathBuf> {
240 for stem in SOURCE_INDEX_FALLBACK_STEMS {
241 for ext in SOURCE_EXTENSIONS {
242 let candidate = base.join(format!("{stem}.{ext}"));
243 if candidate.is_file() {
244 return Some(candidate);
245 }
246 }
247 }
248 None
249}
250
251const DEFAULT_INDEX_PATTERNS: &[&str] = &[
253 "src/index.{ts,tsx,js,jsx}",
254 "src/main.{ts,tsx,js,jsx}",
255 "index.{ts,tsx,js,jsx}",
256 "main.{ts,tsx,js,jsx}",
257];
258
259fn apply_default_fallback(
264 files: &[DiscoveredFile],
265 root: &Path,
266 ws_filter: Option<&Path>,
267) -> Vec<EntryPoint> {
268 let default_matchers: Vec<globset::GlobMatcher> = DEFAULT_INDEX_PATTERNS
269 .iter()
270 .filter_map(|p| globset::Glob::new(p).ok().map(|g| g.compile_matcher()))
271 .collect();
272
273 let mut entries = Vec::new();
274 for file in files {
275 if let Some(ws_root) = ws_filter
277 && file.path.strip_prefix(ws_root).is_err()
278 {
279 continue;
280 }
281 let relative = file.path.strip_prefix(root).unwrap_or(&file.path);
282 let relative_str = relative.to_string_lossy();
283 if default_matchers
284 .iter()
285 .any(|m| m.is_match(relative_str.as_ref()))
286 {
287 entries.push(EntryPoint {
288 path: file.path.clone(),
289 source: EntryPointSource::DefaultIndex,
290 });
291 }
292 }
293 entries
294}
295
296pub fn discover_entry_points(config: &ResolvedConfig, files: &[DiscoveredFile]) -> Vec<EntryPoint> {
298 let _span = tracing::info_span!("discover_entry_points").entered();
299 let mut entries = Vec::new();
300
301 let relative_paths: Vec<String> = files
303 .iter()
304 .map(|f| {
305 f.path
306 .strip_prefix(&config.root)
307 .unwrap_or(&f.path)
308 .to_string_lossy()
309 .into_owned()
310 })
311 .collect();
312
313 {
316 let mut builder = globset::GlobSetBuilder::new();
317 for pattern in &config.entry_patterns {
318 if let Ok(glob) = globset::Glob::new(pattern) {
319 builder.add(glob);
320 }
321 }
322 if let Ok(glob_set) = builder.build()
323 && !glob_set.is_empty()
324 {
325 for (idx, rel) in relative_paths.iter().enumerate() {
326 if glob_set.is_match(rel) {
327 entries.push(EntryPoint {
328 path: files[idx].path.clone(),
329 source: EntryPointSource::ManualEntry,
330 });
331 }
332 }
333 }
334 }
335
336 let canonical_root = dunce::canonicalize(&config.root).unwrap_or_else(|_| config.root.clone());
339 let pkg_path = config.root.join("package.json");
340 let root_pkg = PackageJson::load(&pkg_path).ok();
341 if let Some(pkg) = &root_pkg {
342 for entry_path in pkg.entry_points() {
343 if let Some(ep) = resolve_entry_path(
344 &config.root,
345 &entry_path,
346 &canonical_root,
347 EntryPointSource::PackageJsonMain,
348 ) {
349 entries.push(ep);
350 }
351 }
352
353 if let Some(scripts) = &pkg.scripts {
355 for script_value in scripts.values() {
356 for file_ref in extract_script_file_refs(script_value) {
357 if let Some(ep) = resolve_entry_path(
358 &config.root,
359 &file_ref,
360 &canonical_root,
361 EntryPointSource::PackageJsonScript,
362 ) {
363 entries.push(ep);
364 }
365 }
366 }
367 }
368
369 }
371
372 let exports_dirs = root_pkg
376 .map(|pkg| pkg.exports_subdirectories())
377 .unwrap_or_default();
378 discover_nested_package_entries(
379 &config.root,
380 files,
381 &mut entries,
382 &canonical_root,
383 &exports_dirs,
384 );
385
386 if entries.is_empty() {
388 entries = apply_default_fallback(files, &config.root, None);
389 }
390
391 entries.sort_by(|a, b| a.path.cmp(&b.path));
393 entries.dedup_by(|a, b| a.path == b.path);
394
395 entries
396}
397
398fn discover_nested_package_entries(
408 root: &Path,
409 _files: &[DiscoveredFile],
410 entries: &mut Vec<EntryPoint>,
411 canonical_root: &Path,
412 exports_subdirectories: &[String],
413) {
414 let mut visited = rustc_hash::FxHashSet::default();
415
416 let search_dirs = [
418 "packages", "apps", "libs", "modules", "plugins", "services", "tools", "utils",
419 ];
420 for dir_name in &search_dirs {
421 let search_dir = root.join(dir_name);
422 if !search_dir.is_dir() {
423 continue;
424 }
425 let Ok(read_dir) = std::fs::read_dir(&search_dir) else {
426 continue;
427 };
428 for entry in read_dir.flatten() {
429 let pkg_dir = entry.path();
430 if visited.insert(pkg_dir.clone()) {
431 collect_nested_package_entries(&pkg_dir, entries, canonical_root);
432 }
433 }
434 }
435
436 for dir_name in exports_subdirectories {
438 let pkg_dir = root.join(dir_name);
439 if pkg_dir.is_dir() && visited.insert(pkg_dir.clone()) {
440 collect_nested_package_entries(&pkg_dir, entries, canonical_root);
441 }
442 }
443}
444
445fn collect_nested_package_entries(
447 pkg_dir: &Path,
448 entries: &mut Vec<EntryPoint>,
449 canonical_root: &Path,
450) {
451 let pkg_path = pkg_dir.join("package.json");
452 if !pkg_path.exists() {
453 return;
454 }
455 let Ok(pkg) = PackageJson::load(&pkg_path) else {
456 return;
457 };
458 for entry_path in pkg.entry_points() {
459 if entry_path.contains('*') {
460 expand_wildcard_entries(pkg_dir, &entry_path, canonical_root, entries);
461 } else if let Some(ep) = resolve_entry_path(
462 pkg_dir,
463 &entry_path,
464 canonical_root,
465 EntryPointSource::PackageJsonExports,
466 ) {
467 entries.push(ep);
468 }
469 }
470 if let Some(scripts) = &pkg.scripts {
471 for script_value in scripts.values() {
472 for file_ref in extract_script_file_refs(script_value) {
473 if let Some(ep) = resolve_entry_path(
474 pkg_dir,
475 &file_ref,
476 canonical_root,
477 EntryPointSource::PackageJsonScript,
478 ) {
479 entries.push(ep);
480 }
481 }
482 }
483 }
484}
485
486fn expand_wildcard_entries(
492 base: &Path,
493 pattern: &str,
494 canonical_root: &Path,
495 entries: &mut Vec<EntryPoint>,
496) {
497 let full_pattern = base.join(pattern).to_string_lossy().to_string();
498 let Ok(matches) = glob::glob(&full_pattern) else {
499 return;
500 };
501 for path_result in matches {
502 let Ok(path) = path_result else {
503 continue;
504 };
505 if let Ok(canonical) = dunce::canonicalize(&path)
506 && canonical.starts_with(canonical_root)
507 {
508 entries.push(EntryPoint {
509 path,
510 source: EntryPointSource::PackageJsonExports,
511 });
512 }
513 }
514}
515
516#[must_use]
518pub fn discover_workspace_entry_points(
519 ws_root: &Path,
520 _config: &ResolvedConfig,
521 all_files: &[DiscoveredFile],
522) -> Vec<EntryPoint> {
523 let mut entries = Vec::new();
524
525 let pkg_path = ws_root.join("package.json");
526 if let Ok(pkg) = PackageJson::load(&pkg_path) {
527 let canonical_ws_root =
528 dunce::canonicalize(ws_root).unwrap_or_else(|_| ws_root.to_path_buf());
529 for entry_path in pkg.entry_points() {
530 if entry_path.contains('*') {
531 expand_wildcard_entries(ws_root, &entry_path, &canonical_ws_root, &mut entries);
532 } else if let Some(ep) = resolve_entry_path(
533 ws_root,
534 &entry_path,
535 &canonical_ws_root,
536 EntryPointSource::PackageJsonMain,
537 ) {
538 entries.push(ep);
539 }
540 }
541
542 if let Some(scripts) = &pkg.scripts {
544 for script_value in scripts.values() {
545 for file_ref in extract_script_file_refs(script_value) {
546 if let Some(ep) = resolve_entry_path(
547 ws_root,
548 &file_ref,
549 &canonical_ws_root,
550 EntryPointSource::PackageJsonScript,
551 ) {
552 entries.push(ep);
553 }
554 }
555 }
556 }
557
558 }
560
561 if entries.is_empty() {
563 entries = apply_default_fallback(all_files, ws_root, Some(ws_root));
564 }
565
566 entries.sort_by(|a, b| a.path.cmp(&b.path));
567 entries.dedup_by(|a, b| a.path == b.path);
568 entries
569}
570
571#[must_use]
576pub fn discover_plugin_entry_points(
577 plugin_result: &crate::plugins::AggregatedPluginResult,
578 config: &ResolvedConfig,
579 files: &[DiscoveredFile],
580) -> Vec<EntryPoint> {
581 discover_plugin_entry_point_sets(plugin_result, config, files).all
582}
583
584#[must_use]
586pub fn discover_plugin_entry_point_sets(
587 plugin_result: &crate::plugins::AggregatedPluginResult,
588 config: &ResolvedConfig,
589 files: &[DiscoveredFile],
590) -> CategorizedEntryPoints {
591 let mut entries = CategorizedEntryPoints::default();
592
593 let relative_paths: Vec<String> = files
595 .iter()
596 .map(|f| {
597 f.path
598 .strip_prefix(&config.root)
599 .unwrap_or(&f.path)
600 .to_string_lossy()
601 .into_owned()
602 })
603 .collect();
604
605 let mut builder = globset::GlobSetBuilder::new();
608 let mut glob_meta: Vec<CompiledEntryRule<'_>> = Vec::new();
609 for (rule, pname) in &plugin_result.entry_patterns {
610 if let Some((include, compiled)) = compile_entry_rule(rule, pname, plugin_result) {
611 builder.add(include);
612 glob_meta.push(compiled);
613 }
614 }
615 for (pattern, pname) in plugin_result
616 .discovered_always_used
617 .iter()
618 .chain(plugin_result.always_used.iter())
619 .chain(plugin_result.fixture_patterns.iter())
620 {
621 if let Ok(glob) = globset::GlobBuilder::new(pattern)
622 .literal_separator(true)
623 .build()
624 {
625 builder.add(glob);
626 if let Some(path) = crate::plugins::CompiledPathRule::for_entry_rule(
627 &crate::plugins::PathRule::new(pattern.clone()),
628 "support entry pattern",
629 ) {
630 glob_meta.push(CompiledEntryRule {
631 path,
632 plugin_name: pname,
633 role: EntryPointRole::Support,
634 });
635 }
636 }
637 }
638 if let Ok(glob_set) = builder.build()
639 && !glob_set.is_empty()
640 {
641 for (idx, rel) in relative_paths.iter().enumerate() {
642 let matches: Vec<usize> = glob_set
643 .matches(rel)
644 .into_iter()
645 .filter(|match_idx| glob_meta[*match_idx].matches(rel))
646 .collect();
647 if !matches.is_empty() {
648 let name = glob_meta[matches[0]].plugin_name;
649 let entry = EntryPoint {
650 path: files[idx].path.clone(),
651 source: EntryPointSource::Plugin {
652 name: name.to_string(),
653 },
654 };
655
656 let mut has_runtime = false;
657 let mut has_test = false;
658 let mut has_support = false;
659 for match_idx in matches {
660 match glob_meta[match_idx].role {
661 EntryPointRole::Runtime => has_runtime = true,
662 EntryPointRole::Test => has_test = true,
663 EntryPointRole::Support => has_support = true,
664 }
665 }
666
667 if has_runtime {
668 entries.push_runtime(entry.clone());
669 }
670 if has_test {
671 entries.push_test(entry.clone());
672 }
673 if has_support || (!has_runtime && !has_test) {
674 entries.push_support(entry);
675 }
676 }
677 }
678 }
679
680 for (setup_file, pname) in &plugin_result.setup_files {
682 let resolved = if setup_file.is_absolute() {
683 setup_file.clone()
684 } else {
685 config.root.join(setup_file)
686 };
687 if resolved.exists() {
688 entries.push_support(EntryPoint {
689 path: resolved,
690 source: EntryPointSource::Plugin {
691 name: pname.clone(),
692 },
693 });
694 } else {
695 for ext in SOURCE_EXTENSIONS {
697 let with_ext = resolved.with_extension(ext);
698 if with_ext.exists() {
699 entries.push_support(EntryPoint {
700 path: with_ext,
701 source: EntryPointSource::Plugin {
702 name: pname.clone(),
703 },
704 });
705 break;
706 }
707 }
708 }
709 }
710
711 entries.dedup()
712}
713
714#[must_use]
719pub fn discover_dynamically_loaded_entry_points(
720 config: &ResolvedConfig,
721 files: &[DiscoveredFile],
722) -> Vec<EntryPoint> {
723 if config.dynamically_loaded.is_empty() {
724 return Vec::new();
725 }
726
727 let mut builder = globset::GlobSetBuilder::new();
728 for pattern in &config.dynamically_loaded {
729 if let Ok(glob) = globset::Glob::new(pattern) {
730 builder.add(glob);
731 }
732 }
733 let Ok(glob_set) = builder.build() else {
734 return Vec::new();
735 };
736 if glob_set.is_empty() {
737 return Vec::new();
738 }
739
740 let mut entries = Vec::new();
741 for file in files {
742 let rel = file
743 .path
744 .strip_prefix(&config.root)
745 .unwrap_or(&file.path)
746 .to_string_lossy();
747 if glob_set.is_match(rel.as_ref()) {
748 entries.push(EntryPoint {
749 path: file.path.clone(),
750 source: EntryPointSource::DynamicallyLoaded,
751 });
752 }
753 }
754 entries
755}
756
757struct CompiledEntryRule<'a> {
758 path: crate::plugins::CompiledPathRule,
759 plugin_name: &'a str,
760 role: EntryPointRole,
761}
762
763impl CompiledEntryRule<'_> {
764 fn matches(&self, path: &str) -> bool {
765 self.path.matches(path)
766 }
767}
768
769fn compile_entry_rule<'a>(
770 rule: &'a crate::plugins::PathRule,
771 plugin_name: &'a str,
772 plugin_result: &'a crate::plugins::AggregatedPluginResult,
773) -> Option<(globset::Glob, CompiledEntryRule<'a>)> {
774 let include = match globset::GlobBuilder::new(&rule.pattern)
775 .literal_separator(true)
776 .build()
777 {
778 Ok(glob) => glob,
779 Err(err) => {
780 tracing::warn!("invalid entry pattern '{}': {err}", rule.pattern);
781 return None;
782 }
783 };
784 let role = plugin_result
785 .entry_point_roles
786 .get(plugin_name)
787 .copied()
788 .unwrap_or(EntryPointRole::Support);
789 Some((
790 include,
791 CompiledEntryRule {
792 path: crate::plugins::CompiledPathRule::for_entry_rule(rule, "entry pattern")?,
793 plugin_name,
794 role,
795 },
796 ))
797}
798
799#[must_use]
801pub fn compile_glob_set(patterns: &[String]) -> Option<globset::GlobSet> {
802 if patterns.is_empty() {
803 return None;
804 }
805 let mut builder = globset::GlobSetBuilder::new();
806 for pattern in patterns {
807 if let Ok(glob) = globset::GlobBuilder::new(pattern)
808 .literal_separator(true)
809 .build()
810 {
811 builder.add(glob);
812 }
813 }
814 builder.build().ok()
815}
816
817#[cfg(test)]
818mod tests {
819 use super::*;
820 use fallow_config::{FallowConfig, OutputFormat, RulesConfig};
821 use fallow_types::discover::FileId;
822 use proptest::prelude::*;
823
824 proptest! {
825 #[test]
827 fn glob_patterns_never_panic_on_compile(
828 prefix in "[a-zA-Z0-9_]{1,20}",
829 ext in prop::sample::select(vec!["ts", "tsx", "js", "jsx", "vue", "svelte", "astro", "mdx"]),
830 ) {
831 let pattern = format!("**/{prefix}*.{ext}");
832 let result = globset::Glob::new(&pattern);
834 prop_assert!(result.is_ok(), "Glob::new should not fail for well-formed patterns");
835 }
836
837 #[test]
839 fn non_source_extensions_not_in_list(
840 ext in prop::sample::select(vec!["py", "rb", "rs", "go", "java", "xml", "yaml", "toml", "md", "txt", "png", "jpg", "wasm", "lock"]),
841 ) {
842 prop_assert!(
843 !SOURCE_EXTENSIONS.contains(&ext),
844 "Extension '{ext}' should NOT be in SOURCE_EXTENSIONS"
845 );
846 }
847
848 #[test]
850 fn compile_glob_set_no_panic(
851 patterns in prop::collection::vec("[a-zA-Z0-9_*/.]{1,30}", 0..10),
852 ) {
853 let _ = compile_glob_set(&patterns);
855 }
856 }
857
858 #[test]
860 fn compile_glob_set_empty_input() {
861 assert!(
862 compile_glob_set(&[]).is_none(),
863 "empty patterns should return None"
864 );
865 }
866
867 #[test]
868 fn compile_glob_set_valid_patterns() {
869 let patterns = vec!["**/*.ts".to_string(), "src/**/*.js".to_string()];
870 let set = compile_glob_set(&patterns);
871 assert!(set.is_some(), "valid patterns should compile");
872 let set = set.unwrap();
873 assert!(set.is_match("src/foo.ts"));
874 assert!(set.is_match("src/bar.js"));
875 assert!(!set.is_match("src/bar.py"));
876 }
877
878 #[test]
879 fn compile_glob_set_keeps_star_within_a_single_path_segment() {
880 let patterns = vec!["composables/*.{ts,js}".to_string()];
881 let set = compile_glob_set(&patterns).expect("pattern should compile");
882
883 assert!(set.is_match("composables/useFoo.ts"));
884 assert!(!set.is_match("composables/nested/useFoo.ts"));
885 }
886
887 #[test]
888 fn plugin_entry_point_sets_preserve_runtime_test_and_support_roles() {
889 let dir = tempfile::tempdir().expect("create temp dir");
890 let root = dir.path();
891 std::fs::create_dir_all(root.join("src")).unwrap();
892 std::fs::create_dir_all(root.join("tests")).unwrap();
893 std::fs::write(root.join("src/runtime.ts"), "export const runtime = 1;").unwrap();
894 std::fs::write(root.join("src/setup.ts"), "export const setup = 1;").unwrap();
895 std::fs::write(root.join("tests/app.test.ts"), "export const test = 1;").unwrap();
896
897 let config = FallowConfig {
898 schema: None,
899 extends: vec![],
900 entry: vec![],
901 ignore_patterns: vec![],
902 framework: vec![],
903 workspaces: None,
904 ignore_dependencies: vec![],
905 ignore_exports: vec![],
906 used_class_members: vec![],
907 duplicates: fallow_config::DuplicatesConfig::default(),
908 health: fallow_config::HealthConfig::default(),
909 rules: RulesConfig::default(),
910 boundaries: fallow_config::BoundaryConfig::default(),
911 production: false,
912 plugins: vec![],
913 dynamically_loaded: vec![],
914 overrides: vec![],
915 regression: None,
916 audit: fallow_config::AuditConfig::default(),
917 codeowners: None,
918 public_packages: vec![],
919 flags: fallow_config::FlagsConfig::default(),
920 sealed: false,
921 }
922 .resolve(root.to_path_buf(), OutputFormat::Human, 4, true, true);
923
924 let files = vec![
925 DiscoveredFile {
926 id: FileId(0),
927 path: root.join("src/runtime.ts"),
928 size_bytes: 1,
929 },
930 DiscoveredFile {
931 id: FileId(1),
932 path: root.join("src/setup.ts"),
933 size_bytes: 1,
934 },
935 DiscoveredFile {
936 id: FileId(2),
937 path: root.join("tests/app.test.ts"),
938 size_bytes: 1,
939 },
940 ];
941
942 let mut plugin_result = crate::plugins::AggregatedPluginResult::default();
943 plugin_result.entry_patterns.push((
944 crate::plugins::PathRule::new("src/runtime.ts"),
945 "runtime-plugin".to_string(),
946 ));
947 plugin_result.entry_patterns.push((
948 crate::plugins::PathRule::new("tests/app.test.ts"),
949 "test-plugin".to_string(),
950 ));
951 plugin_result
952 .always_used
953 .push(("src/setup.ts".to_string(), "support-plugin".to_string()));
954 plugin_result
955 .entry_point_roles
956 .insert("runtime-plugin".to_string(), EntryPointRole::Runtime);
957 plugin_result
958 .entry_point_roles
959 .insert("test-plugin".to_string(), EntryPointRole::Test);
960 plugin_result
961 .entry_point_roles
962 .insert("support-plugin".to_string(), EntryPointRole::Support);
963
964 let entries = discover_plugin_entry_point_sets(&plugin_result, &config, &files);
965
966 assert_eq!(entries.runtime.len(), 1, "expected one runtime entry");
967 assert!(
968 entries.runtime[0].path.ends_with("src/runtime.ts"),
969 "runtime entry should stay runtime-only"
970 );
971 assert_eq!(entries.test.len(), 1, "expected one test entry");
972 assert!(
973 entries.test[0].path.ends_with("tests/app.test.ts"),
974 "test entry should stay test-only"
975 );
976 assert_eq!(
977 entries.all.len(),
978 3,
979 "support entries should stay in all entries"
980 );
981 assert!(
982 entries
983 .all
984 .iter()
985 .any(|entry| entry.path.ends_with("src/setup.ts")),
986 "support entries should remain in the overall entry-point set"
987 );
988 assert!(
989 !entries
990 .runtime
991 .iter()
992 .any(|entry| entry.path.ends_with("src/setup.ts")),
993 "support entries should not bleed into runtime reachability"
994 );
995 assert!(
996 !entries
997 .test
998 .iter()
999 .any(|entry| entry.path.ends_with("src/setup.ts")),
1000 "support entries should not bleed into test reachability"
1001 );
1002 }
1003
1004 #[test]
1005 fn plugin_entry_point_rules_respect_exclusions() {
1006 let dir = tempfile::tempdir().expect("create temp dir");
1007 let root = dir.path();
1008 std::fs::create_dir_all(root.join("app/pages")).unwrap();
1009 std::fs::write(
1010 root.join("app/pages/index.tsx"),
1011 "export default function Page() { return null; }",
1012 )
1013 .unwrap();
1014 std::fs::write(
1015 root.join("app/pages/-helper.ts"),
1016 "export const helper = 1;",
1017 )
1018 .unwrap();
1019
1020 let config = FallowConfig {
1021 schema: None,
1022 extends: vec![],
1023 entry: vec![],
1024 ignore_patterns: vec![],
1025 framework: vec![],
1026 workspaces: None,
1027 ignore_dependencies: vec![],
1028 ignore_exports: vec![],
1029 used_class_members: vec![],
1030 duplicates: fallow_config::DuplicatesConfig::default(),
1031 health: fallow_config::HealthConfig::default(),
1032 rules: RulesConfig::default(),
1033 boundaries: fallow_config::BoundaryConfig::default(),
1034 production: false,
1035 plugins: vec![],
1036 dynamically_loaded: vec![],
1037 overrides: vec![],
1038 regression: None,
1039 audit: fallow_config::AuditConfig::default(),
1040 codeowners: None,
1041 public_packages: vec![],
1042 flags: fallow_config::FlagsConfig::default(),
1043 sealed: false,
1044 }
1045 .resolve(root.to_path_buf(), OutputFormat::Human, 4, true, true);
1046
1047 let files = vec![
1048 DiscoveredFile {
1049 id: FileId(0),
1050 path: root.join("app/pages/index.tsx"),
1051 size_bytes: 1,
1052 },
1053 DiscoveredFile {
1054 id: FileId(1),
1055 path: root.join("app/pages/-helper.ts"),
1056 size_bytes: 1,
1057 },
1058 ];
1059
1060 let mut plugin_result = crate::plugins::AggregatedPluginResult::default();
1061 plugin_result.entry_patterns.push((
1062 crate::plugins::PathRule::new("app/pages/**/*.{ts,tsx,js,jsx}")
1063 .with_excluded_globs(["app/pages/**/-*", "app/pages/**/-*/**/*"]),
1064 "tanstack-router".to_string(),
1065 ));
1066 plugin_result
1067 .entry_point_roles
1068 .insert("tanstack-router".to_string(), EntryPointRole::Runtime);
1069
1070 let entries = discover_plugin_entry_point_sets(&plugin_result, &config, &files);
1071 let entry_paths: Vec<_> = entries
1072 .all
1073 .iter()
1074 .map(|entry| {
1075 entry
1076 .path
1077 .strip_prefix(root)
1078 .unwrap()
1079 .to_string_lossy()
1080 .into_owned()
1081 })
1082 .collect();
1083
1084 assert!(entry_paths.contains(&"app/pages/index.tsx".to_string()));
1085 assert!(!entry_paths.contains(&"app/pages/-helper.ts".to_string()));
1086 }
1087
1088 mod resolve_entry_path_tests {
1090 use super::*;
1091
1092 #[test]
1093 fn resolves_existing_file() {
1094 let dir = tempfile::tempdir().expect("create temp dir");
1095 let src = dir.path().join("src");
1096 std::fs::create_dir_all(&src).unwrap();
1097 std::fs::write(src.join("index.ts"), "export const a = 1;").unwrap();
1098
1099 let canonical = dunce::canonicalize(dir.path()).unwrap();
1100 let result = resolve_entry_path(
1101 dir.path(),
1102 "src/index.ts",
1103 &canonical,
1104 EntryPointSource::PackageJsonMain,
1105 );
1106 assert!(result.is_some(), "should resolve an existing file");
1107 assert!(result.unwrap().path.ends_with("src/index.ts"));
1108 }
1109
1110 #[test]
1111 fn resolves_with_extension_fallback() {
1112 let dir = tempfile::tempdir().expect("create temp dir");
1113 let canonical = dunce::canonicalize(dir.path()).unwrap();
1115 let src = canonical.join("src");
1116 std::fs::create_dir_all(&src).unwrap();
1117 std::fs::write(src.join("index.ts"), "export const a = 1;").unwrap();
1118
1119 let result = resolve_entry_path(
1121 &canonical,
1122 "src/index",
1123 &canonical,
1124 EntryPointSource::PackageJsonMain,
1125 );
1126 assert!(
1127 result.is_some(),
1128 "should resolve via extension fallback when exact path doesn't exist"
1129 );
1130 let ep = result.unwrap();
1131 assert!(
1132 ep.path.to_string_lossy().contains("index.ts"),
1133 "should find index.ts via extension fallback"
1134 );
1135 }
1136
1137 #[test]
1138 fn returns_none_for_nonexistent_file() {
1139 let dir = tempfile::tempdir().expect("create temp dir");
1140 let canonical = dunce::canonicalize(dir.path()).unwrap();
1141 let result = resolve_entry_path(
1142 dir.path(),
1143 "does/not/exist.ts",
1144 &canonical,
1145 EntryPointSource::PackageJsonMain,
1146 );
1147 assert!(result.is_none(), "should return None for nonexistent files");
1148 }
1149
1150 #[test]
1151 fn maps_dist_output_to_src() {
1152 let dir = tempfile::tempdir().expect("create temp dir");
1153 let src = dir.path().join("src");
1154 std::fs::create_dir_all(&src).unwrap();
1155 std::fs::write(src.join("utils.ts"), "export const u = 1;").unwrap();
1156
1157 let dist = dir.path().join("dist");
1159 std::fs::create_dir_all(&dist).unwrap();
1160 std::fs::write(dist.join("utils.js"), "// compiled").unwrap();
1161
1162 let canonical = dunce::canonicalize(dir.path()).unwrap();
1163 let result = resolve_entry_path(
1164 dir.path(),
1165 "./dist/utils.js",
1166 &canonical,
1167 EntryPointSource::PackageJsonExports,
1168 );
1169 assert!(result.is_some(), "should resolve dist/ path to src/");
1170 let ep = result.unwrap();
1171 assert!(
1172 ep.path
1173 .to_string_lossy()
1174 .replace('\\', "/")
1175 .contains("src/utils.ts"),
1176 "should map ./dist/utils.js to src/utils.ts"
1177 );
1178 }
1179
1180 #[test]
1181 fn maps_build_output_to_src() {
1182 let dir = tempfile::tempdir().expect("create temp dir");
1183 let canonical = dunce::canonicalize(dir.path()).unwrap();
1185 let src = canonical.join("src");
1186 std::fs::create_dir_all(&src).unwrap();
1187 std::fs::write(src.join("index.tsx"), "export default () => {};").unwrap();
1188
1189 let result = resolve_entry_path(
1190 &canonical,
1191 "./build/index.js",
1192 &canonical,
1193 EntryPointSource::PackageJsonExports,
1194 );
1195 assert!(result.is_some(), "should map build/ output to src/");
1196 let ep = result.unwrap();
1197 assert!(
1198 ep.path
1199 .to_string_lossy()
1200 .replace('\\', "/")
1201 .contains("src/index.tsx"),
1202 "should map ./build/index.js to src/index.tsx"
1203 );
1204 }
1205
1206 #[test]
1207 fn preserves_entry_point_source() {
1208 let dir = tempfile::tempdir().expect("create temp dir");
1209 std::fs::write(dir.path().join("index.ts"), "export const a = 1;").unwrap();
1210
1211 let canonical = dunce::canonicalize(dir.path()).unwrap();
1212 let result = resolve_entry_path(
1213 dir.path(),
1214 "index.ts",
1215 &canonical,
1216 EntryPointSource::PackageJsonScript,
1217 );
1218 assert!(result.is_some());
1219 assert!(
1220 matches!(result.unwrap().source, EntryPointSource::PackageJsonScript),
1221 "should preserve the source kind"
1222 );
1223 }
1224 }
1225
1226 mod output_to_source_tests {
1228 use super::*;
1229
1230 #[test]
1231 fn maps_dist_to_src_with_ts_extension() {
1232 let dir = tempfile::tempdir().expect("create temp dir");
1233 let src = dir.path().join("src");
1234 std::fs::create_dir_all(&src).unwrap();
1235 std::fs::write(src.join("utils.ts"), "export const u = 1;").unwrap();
1236
1237 let result = try_output_to_source_path(dir.path(), "./dist/utils.js");
1238 assert!(result.is_some());
1239 assert!(
1240 result
1241 .unwrap()
1242 .to_string_lossy()
1243 .replace('\\', "/")
1244 .contains("src/utils.ts")
1245 );
1246 }
1247
1248 #[test]
1249 fn returns_none_when_no_source_file_exists() {
1250 let dir = tempfile::tempdir().expect("create temp dir");
1251 let result = try_output_to_source_path(dir.path(), "./dist/missing.js");
1253 assert!(result.is_none());
1254 }
1255
1256 #[test]
1257 fn ignores_non_output_directories() {
1258 let dir = tempfile::tempdir().expect("create temp dir");
1259 let src = dir.path().join("src");
1260 std::fs::create_dir_all(&src).unwrap();
1261 std::fs::write(src.join("foo.ts"), "export const f = 1;").unwrap();
1262
1263 let result = try_output_to_source_path(dir.path(), "./lib/foo.js");
1265 assert!(result.is_none());
1266 }
1267
1268 #[test]
1269 fn maps_nested_output_path_preserving_prefix() {
1270 let dir = tempfile::tempdir().expect("create temp dir");
1271 let modules_src = dir.path().join("modules").join("src");
1272 std::fs::create_dir_all(&modules_src).unwrap();
1273 std::fs::write(modules_src.join("helper.ts"), "export const h = 1;").unwrap();
1274
1275 let result = try_output_to_source_path(dir.path(), "./modules/dist/helper.js");
1276 assert!(result.is_some());
1277 assert!(
1278 result
1279 .unwrap()
1280 .to_string_lossy()
1281 .replace('\\', "/")
1282 .contains("modules/src/helper.ts")
1283 );
1284 }
1285 }
1286
1287 mod source_index_fallback_tests {
1289 use super::*;
1290
1291 #[test]
1292 fn detects_dist_entry_in_output_dir() {
1293 assert!(is_entry_in_output_dir("./dist/esm2022/index.js"));
1294 assert!(is_entry_in_output_dir("dist/index.js"));
1295 assert!(is_entry_in_output_dir("./build/index.js"));
1296 assert!(is_entry_in_output_dir("./out/main.js"));
1297 assert!(is_entry_in_output_dir("./esm/index.js"));
1298 assert!(is_entry_in_output_dir("./cjs/index.js"));
1299 }
1300
1301 #[test]
1302 fn rejects_non_output_entry_paths() {
1303 assert!(!is_entry_in_output_dir("./src/index.ts"));
1304 assert!(!is_entry_in_output_dir("src/main.ts"));
1305 assert!(!is_entry_in_output_dir("./index.js"));
1306 assert!(!is_entry_in_output_dir(""));
1307 }
1308
1309 #[test]
1310 fn rejects_substring_match_for_output_dir() {
1311 assert!(!is_entry_in_output_dir("./distro/index.js"));
1313 assert!(!is_entry_in_output_dir("./build-scripts/run.js"));
1314 }
1315
1316 #[test]
1317 fn finds_src_index_ts() {
1318 let dir = tempfile::tempdir().expect("create temp dir");
1319 let src = dir.path().join("src");
1320 std::fs::create_dir_all(&src).unwrap();
1321 let index_path = src.join("index.ts");
1322 std::fs::write(&index_path, "export const a = 1;").unwrap();
1323
1324 let result = try_source_index_fallback(dir.path());
1325 assert_eq!(result.as_deref(), Some(index_path.as_path()));
1326 }
1327
1328 #[test]
1329 fn finds_src_index_tsx_when_ts_missing() {
1330 let dir = tempfile::tempdir().expect("create temp dir");
1331 let src = dir.path().join("src");
1332 std::fs::create_dir_all(&src).unwrap();
1333 let index_path = src.join("index.tsx");
1334 std::fs::write(&index_path, "export default 1;").unwrap();
1335
1336 let result = try_source_index_fallback(dir.path());
1337 assert_eq!(result.as_deref(), Some(index_path.as_path()));
1338 }
1339
1340 #[test]
1341 fn prefers_src_index_over_root_index() {
1342 let dir = tempfile::tempdir().expect("create temp dir");
1345 let src = dir.path().join("src");
1346 std::fs::create_dir_all(&src).unwrap();
1347 let src_index = src.join("index.ts");
1348 std::fs::write(&src_index, "export const a = 1;").unwrap();
1349 let root_index = dir.path().join("index.ts");
1350 std::fs::write(&root_index, "export const b = 2;").unwrap();
1351
1352 let result = try_source_index_fallback(dir.path());
1353 assert_eq!(result.as_deref(), Some(src_index.as_path()));
1354 }
1355
1356 #[test]
1357 fn falls_back_to_src_main() {
1358 let dir = tempfile::tempdir().expect("create temp dir");
1359 let src = dir.path().join("src");
1360 std::fs::create_dir_all(&src).unwrap();
1361 let main_path = src.join("main.ts");
1362 std::fs::write(&main_path, "export const a = 1;").unwrap();
1363
1364 let result = try_source_index_fallback(dir.path());
1365 assert_eq!(result.as_deref(), Some(main_path.as_path()));
1366 }
1367
1368 #[test]
1369 fn falls_back_to_root_index_when_no_src() {
1370 let dir = tempfile::tempdir().expect("create temp dir");
1371 let index_path = dir.path().join("index.js");
1372 std::fs::write(&index_path, "module.exports = {};").unwrap();
1373
1374 let result = try_source_index_fallback(dir.path());
1375 assert_eq!(result.as_deref(), Some(index_path.as_path()));
1376 }
1377
1378 #[test]
1379 fn returns_none_when_nothing_matches() {
1380 let dir = tempfile::tempdir().expect("create temp dir");
1381 let result = try_source_index_fallback(dir.path());
1382 assert!(result.is_none());
1383 }
1384
1385 #[test]
1386 fn resolve_entry_path_falls_back_to_src_index_for_dist_entry() {
1387 let dir = tempfile::tempdir().expect("create temp dir");
1388 let canonical = dunce::canonicalize(dir.path()).unwrap();
1389
1390 let dist_dir = canonical.join("dist").join("esm2022");
1395 std::fs::create_dir_all(&dist_dir).unwrap();
1396 std::fs::write(dist_dir.join("index.js"), "export const x = 1;").unwrap();
1397
1398 let src = canonical.join("src");
1399 std::fs::create_dir_all(&src).unwrap();
1400 let src_index = src.join("index.ts");
1401 std::fs::write(&src_index, "export const x = 1;").unwrap();
1402
1403 let result = resolve_entry_path(
1404 &canonical,
1405 "./dist/esm2022/index.js",
1406 &canonical,
1407 EntryPointSource::PackageJsonMain,
1408 );
1409 assert!(result.is_some());
1410 let entry = result.unwrap();
1411 assert_eq!(entry.path, src_index);
1412 }
1413
1414 #[test]
1415 fn resolve_entry_path_uses_direct_src_mirror_when_available() {
1416 let dir = tempfile::tempdir().expect("create temp dir");
1419 let canonical = dunce::canonicalize(dir.path()).unwrap();
1420
1421 let src_mirror = canonical.join("src").join("esm2022");
1422 std::fs::create_dir_all(&src_mirror).unwrap();
1423 let mirror_index = src_mirror.join("index.ts");
1424 std::fs::write(&mirror_index, "export const x = 1;").unwrap();
1425
1426 let src_index = canonical.join("src").join("index.ts");
1428 std::fs::write(&src_index, "export const y = 2;").unwrap();
1429
1430 let result = resolve_entry_path(
1431 &canonical,
1432 "./dist/esm2022/index.js",
1433 &canonical,
1434 EntryPointSource::PackageJsonMain,
1435 );
1436 assert_eq!(result.map(|e| e.path), Some(mirror_index));
1437 }
1438 }
1439
1440 mod default_fallback_tests {
1442 use super::*;
1443
1444 #[test]
1445 fn finds_src_index_ts_as_fallback() {
1446 let dir = tempfile::tempdir().expect("create temp dir");
1447 let src = dir.path().join("src");
1448 std::fs::create_dir_all(&src).unwrap();
1449 let index_path = src.join("index.ts");
1450 std::fs::write(&index_path, "export const a = 1;").unwrap();
1451
1452 let files = vec![DiscoveredFile {
1453 id: FileId(0),
1454 path: index_path.clone(),
1455 size_bytes: 20,
1456 }];
1457
1458 let entries = apply_default_fallback(&files, dir.path(), None);
1459 assert_eq!(entries.len(), 1);
1460 assert_eq!(entries[0].path, index_path);
1461 assert!(matches!(entries[0].source, EntryPointSource::DefaultIndex));
1462 }
1463
1464 #[test]
1465 fn finds_root_index_js_as_fallback() {
1466 let dir = tempfile::tempdir().expect("create temp dir");
1467 let index_path = dir.path().join("index.js");
1468 std::fs::write(&index_path, "module.exports = {};").unwrap();
1469
1470 let files = vec![DiscoveredFile {
1471 id: FileId(0),
1472 path: index_path.clone(),
1473 size_bytes: 21,
1474 }];
1475
1476 let entries = apply_default_fallback(&files, dir.path(), None);
1477 assert_eq!(entries.len(), 1);
1478 assert_eq!(entries[0].path, index_path);
1479 }
1480
1481 #[test]
1482 fn returns_empty_when_no_index_file() {
1483 let dir = tempfile::tempdir().expect("create temp dir");
1484 let other_path = dir.path().join("src").join("utils.ts");
1485
1486 let files = vec![DiscoveredFile {
1487 id: FileId(0),
1488 path: other_path,
1489 size_bytes: 10,
1490 }];
1491
1492 let entries = apply_default_fallback(&files, dir.path(), None);
1493 assert!(
1494 entries.is_empty(),
1495 "non-index files should not match default fallback"
1496 );
1497 }
1498
1499 #[test]
1500 fn workspace_filter_restricts_scope() {
1501 let dir = tempfile::tempdir().expect("create temp dir");
1502 let ws_a = dir.path().join("packages").join("a").join("src");
1503 std::fs::create_dir_all(&ws_a).unwrap();
1504 let ws_b = dir.path().join("packages").join("b").join("src");
1505 std::fs::create_dir_all(&ws_b).unwrap();
1506
1507 let index_a = ws_a.join("index.ts");
1508 let index_b = ws_b.join("index.ts");
1509
1510 let files = vec![
1511 DiscoveredFile {
1512 id: FileId(0),
1513 path: index_a.clone(),
1514 size_bytes: 10,
1515 },
1516 DiscoveredFile {
1517 id: FileId(1),
1518 path: index_b,
1519 size_bytes: 10,
1520 },
1521 ];
1522
1523 let ws_root = dir.path().join("packages").join("a");
1525 let entries = apply_default_fallback(&files, &ws_root, Some(&ws_root));
1526 assert_eq!(entries.len(), 1);
1527 assert_eq!(entries[0].path, index_a);
1528 }
1529 }
1530
1531 mod wildcard_entry_tests {
1533 use super::*;
1534
1535 #[test]
1536 fn expands_wildcard_css_entries() {
1537 let dir = tempfile::tempdir().expect("create temp dir");
1540 let themes = dir.path().join("src").join("themes");
1541 std::fs::create_dir_all(&themes).unwrap();
1542 std::fs::write(themes.join("dark.css"), ":root { --bg: #000; }").unwrap();
1543 std::fs::write(themes.join("light.css"), ":root { --bg: #fff; }").unwrap();
1544
1545 let canonical = dunce::canonicalize(dir.path()).unwrap();
1546 let mut entries = Vec::new();
1547 expand_wildcard_entries(dir.path(), "./src/themes/*.css", &canonical, &mut entries);
1548
1549 assert_eq!(entries.len(), 2, "should expand wildcard to 2 CSS files");
1550 let paths: Vec<String> = entries
1551 .iter()
1552 .map(|ep| ep.path.file_name().unwrap().to_string_lossy().to_string())
1553 .collect();
1554 assert!(paths.contains(&"dark.css".to_string()));
1555 assert!(paths.contains(&"light.css".to_string()));
1556 assert!(
1557 entries
1558 .iter()
1559 .all(|ep| matches!(ep.source, EntryPointSource::PackageJsonExports))
1560 );
1561 }
1562
1563 #[test]
1564 fn wildcard_does_not_match_nonexistent_files() {
1565 let dir = tempfile::tempdir().expect("create temp dir");
1566 std::fs::create_dir_all(dir.path().join("src/themes")).unwrap();
1568
1569 let canonical = dunce::canonicalize(dir.path()).unwrap();
1570 let mut entries = Vec::new();
1571 expand_wildcard_entries(dir.path(), "./src/themes/*.css", &canonical, &mut entries);
1572
1573 assert!(
1574 entries.is_empty(),
1575 "should return empty when no files match the wildcard"
1576 );
1577 }
1578
1579 #[test]
1580 fn wildcard_only_matches_specified_extension() {
1581 let dir = tempfile::tempdir().expect("create temp dir");
1583 let themes = dir.path().join("src").join("themes");
1584 std::fs::create_dir_all(&themes).unwrap();
1585 std::fs::write(themes.join("dark.css"), ":root {}").unwrap();
1586 std::fs::write(themes.join("index.ts"), "export {};").unwrap();
1587
1588 let canonical = dunce::canonicalize(dir.path()).unwrap();
1589 let mut entries = Vec::new();
1590 expand_wildcard_entries(dir.path(), "./src/themes/*.css", &canonical, &mut entries);
1591
1592 assert_eq!(entries.len(), 1, "should only match CSS files");
1593 assert!(
1594 entries[0]
1595 .path
1596 .file_name()
1597 .unwrap()
1598 .to_string_lossy()
1599 .ends_with(".css")
1600 );
1601 }
1602 }
1603}