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 codeowners: None,
917 public_packages: vec![],
918 flags: fallow_config::FlagsConfig::default(),
919 }
920 .resolve(root.to_path_buf(), OutputFormat::Human, 4, true, true);
921
922 let files = vec![
923 DiscoveredFile {
924 id: FileId(0),
925 path: root.join("src/runtime.ts"),
926 size_bytes: 1,
927 },
928 DiscoveredFile {
929 id: FileId(1),
930 path: root.join("src/setup.ts"),
931 size_bytes: 1,
932 },
933 DiscoveredFile {
934 id: FileId(2),
935 path: root.join("tests/app.test.ts"),
936 size_bytes: 1,
937 },
938 ];
939
940 let mut plugin_result = crate::plugins::AggregatedPluginResult::default();
941 plugin_result.entry_patterns.push((
942 crate::plugins::PathRule::new("src/runtime.ts"),
943 "runtime-plugin".to_string(),
944 ));
945 plugin_result.entry_patterns.push((
946 crate::plugins::PathRule::new("tests/app.test.ts"),
947 "test-plugin".to_string(),
948 ));
949 plugin_result
950 .always_used
951 .push(("src/setup.ts".to_string(), "support-plugin".to_string()));
952 plugin_result
953 .entry_point_roles
954 .insert("runtime-plugin".to_string(), EntryPointRole::Runtime);
955 plugin_result
956 .entry_point_roles
957 .insert("test-plugin".to_string(), EntryPointRole::Test);
958 plugin_result
959 .entry_point_roles
960 .insert("support-plugin".to_string(), EntryPointRole::Support);
961
962 let entries = discover_plugin_entry_point_sets(&plugin_result, &config, &files);
963
964 assert_eq!(entries.runtime.len(), 1, "expected one runtime entry");
965 assert!(
966 entries.runtime[0].path.ends_with("src/runtime.ts"),
967 "runtime entry should stay runtime-only"
968 );
969 assert_eq!(entries.test.len(), 1, "expected one test entry");
970 assert!(
971 entries.test[0].path.ends_with("tests/app.test.ts"),
972 "test entry should stay test-only"
973 );
974 assert_eq!(
975 entries.all.len(),
976 3,
977 "support entries should stay in all entries"
978 );
979 assert!(
980 entries
981 .all
982 .iter()
983 .any(|entry| entry.path.ends_with("src/setup.ts")),
984 "support entries should remain in the overall entry-point set"
985 );
986 assert!(
987 !entries
988 .runtime
989 .iter()
990 .any(|entry| entry.path.ends_with("src/setup.ts")),
991 "support entries should not bleed into runtime reachability"
992 );
993 assert!(
994 !entries
995 .test
996 .iter()
997 .any(|entry| entry.path.ends_with("src/setup.ts")),
998 "support entries should not bleed into test reachability"
999 );
1000 }
1001
1002 #[test]
1003 fn plugin_entry_point_rules_respect_exclusions() {
1004 let dir = tempfile::tempdir().expect("create temp dir");
1005 let root = dir.path();
1006 std::fs::create_dir_all(root.join("app/pages")).unwrap();
1007 std::fs::write(
1008 root.join("app/pages/index.tsx"),
1009 "export default function Page() { return null; }",
1010 )
1011 .unwrap();
1012 std::fs::write(
1013 root.join("app/pages/-helper.ts"),
1014 "export const helper = 1;",
1015 )
1016 .unwrap();
1017
1018 let config = FallowConfig {
1019 schema: None,
1020 extends: vec![],
1021 entry: vec![],
1022 ignore_patterns: vec![],
1023 framework: vec![],
1024 workspaces: None,
1025 ignore_dependencies: vec![],
1026 ignore_exports: vec![],
1027 used_class_members: vec![],
1028 duplicates: fallow_config::DuplicatesConfig::default(),
1029 health: fallow_config::HealthConfig::default(),
1030 rules: RulesConfig::default(),
1031 boundaries: fallow_config::BoundaryConfig::default(),
1032 production: false,
1033 plugins: vec![],
1034 dynamically_loaded: vec![],
1035 overrides: vec![],
1036 regression: None,
1037 codeowners: None,
1038 public_packages: vec![],
1039 flags: fallow_config::FlagsConfig::default(),
1040 }
1041 .resolve(root.to_path_buf(), OutputFormat::Human, 4, true, true);
1042
1043 let files = vec![
1044 DiscoveredFile {
1045 id: FileId(0),
1046 path: root.join("app/pages/index.tsx"),
1047 size_bytes: 1,
1048 },
1049 DiscoveredFile {
1050 id: FileId(1),
1051 path: root.join("app/pages/-helper.ts"),
1052 size_bytes: 1,
1053 },
1054 ];
1055
1056 let mut plugin_result = crate::plugins::AggregatedPluginResult::default();
1057 plugin_result.entry_patterns.push((
1058 crate::plugins::PathRule::new("app/pages/**/*.{ts,tsx,js,jsx}")
1059 .with_excluded_globs(["app/pages/**/-*", "app/pages/**/-*/**/*"]),
1060 "tanstack-router".to_string(),
1061 ));
1062 plugin_result
1063 .entry_point_roles
1064 .insert("tanstack-router".to_string(), EntryPointRole::Runtime);
1065
1066 let entries = discover_plugin_entry_point_sets(&plugin_result, &config, &files);
1067 let entry_paths: Vec<_> = entries
1068 .all
1069 .iter()
1070 .map(|entry| {
1071 entry
1072 .path
1073 .strip_prefix(root)
1074 .unwrap()
1075 .to_string_lossy()
1076 .into_owned()
1077 })
1078 .collect();
1079
1080 assert!(entry_paths.contains(&"app/pages/index.tsx".to_string()));
1081 assert!(!entry_paths.contains(&"app/pages/-helper.ts".to_string()));
1082 }
1083
1084 mod resolve_entry_path_tests {
1086 use super::*;
1087
1088 #[test]
1089 fn resolves_existing_file() {
1090 let dir = tempfile::tempdir().expect("create temp dir");
1091 let src = dir.path().join("src");
1092 std::fs::create_dir_all(&src).unwrap();
1093 std::fs::write(src.join("index.ts"), "export const a = 1;").unwrap();
1094
1095 let canonical = dunce::canonicalize(dir.path()).unwrap();
1096 let result = resolve_entry_path(
1097 dir.path(),
1098 "src/index.ts",
1099 &canonical,
1100 EntryPointSource::PackageJsonMain,
1101 );
1102 assert!(result.is_some(), "should resolve an existing file");
1103 assert!(result.unwrap().path.ends_with("src/index.ts"));
1104 }
1105
1106 #[test]
1107 fn resolves_with_extension_fallback() {
1108 let dir = tempfile::tempdir().expect("create temp dir");
1109 let canonical = dunce::canonicalize(dir.path()).unwrap();
1111 let src = canonical.join("src");
1112 std::fs::create_dir_all(&src).unwrap();
1113 std::fs::write(src.join("index.ts"), "export const a = 1;").unwrap();
1114
1115 let result = resolve_entry_path(
1117 &canonical,
1118 "src/index",
1119 &canonical,
1120 EntryPointSource::PackageJsonMain,
1121 );
1122 assert!(
1123 result.is_some(),
1124 "should resolve via extension fallback when exact path doesn't exist"
1125 );
1126 let ep = result.unwrap();
1127 assert!(
1128 ep.path.to_string_lossy().contains("index.ts"),
1129 "should find index.ts via extension fallback"
1130 );
1131 }
1132
1133 #[test]
1134 fn returns_none_for_nonexistent_file() {
1135 let dir = tempfile::tempdir().expect("create temp dir");
1136 let canonical = dunce::canonicalize(dir.path()).unwrap();
1137 let result = resolve_entry_path(
1138 dir.path(),
1139 "does/not/exist.ts",
1140 &canonical,
1141 EntryPointSource::PackageJsonMain,
1142 );
1143 assert!(result.is_none(), "should return None for nonexistent files");
1144 }
1145
1146 #[test]
1147 fn maps_dist_output_to_src() {
1148 let dir = tempfile::tempdir().expect("create temp dir");
1149 let src = dir.path().join("src");
1150 std::fs::create_dir_all(&src).unwrap();
1151 std::fs::write(src.join("utils.ts"), "export const u = 1;").unwrap();
1152
1153 let dist = dir.path().join("dist");
1155 std::fs::create_dir_all(&dist).unwrap();
1156 std::fs::write(dist.join("utils.js"), "// compiled").unwrap();
1157
1158 let canonical = dunce::canonicalize(dir.path()).unwrap();
1159 let result = resolve_entry_path(
1160 dir.path(),
1161 "./dist/utils.js",
1162 &canonical,
1163 EntryPointSource::PackageJsonExports,
1164 );
1165 assert!(result.is_some(), "should resolve dist/ path to src/");
1166 let ep = result.unwrap();
1167 assert!(
1168 ep.path
1169 .to_string_lossy()
1170 .replace('\\', "/")
1171 .contains("src/utils.ts"),
1172 "should map ./dist/utils.js to src/utils.ts"
1173 );
1174 }
1175
1176 #[test]
1177 fn maps_build_output_to_src() {
1178 let dir = tempfile::tempdir().expect("create temp dir");
1179 let canonical = dunce::canonicalize(dir.path()).unwrap();
1181 let src = canonical.join("src");
1182 std::fs::create_dir_all(&src).unwrap();
1183 std::fs::write(src.join("index.tsx"), "export default () => {};").unwrap();
1184
1185 let result = resolve_entry_path(
1186 &canonical,
1187 "./build/index.js",
1188 &canonical,
1189 EntryPointSource::PackageJsonExports,
1190 );
1191 assert!(result.is_some(), "should map build/ output to src/");
1192 let ep = result.unwrap();
1193 assert!(
1194 ep.path
1195 .to_string_lossy()
1196 .replace('\\', "/")
1197 .contains("src/index.tsx"),
1198 "should map ./build/index.js to src/index.tsx"
1199 );
1200 }
1201
1202 #[test]
1203 fn preserves_entry_point_source() {
1204 let dir = tempfile::tempdir().expect("create temp dir");
1205 std::fs::write(dir.path().join("index.ts"), "export const a = 1;").unwrap();
1206
1207 let canonical = dunce::canonicalize(dir.path()).unwrap();
1208 let result = resolve_entry_path(
1209 dir.path(),
1210 "index.ts",
1211 &canonical,
1212 EntryPointSource::PackageJsonScript,
1213 );
1214 assert!(result.is_some());
1215 assert!(
1216 matches!(result.unwrap().source, EntryPointSource::PackageJsonScript),
1217 "should preserve the source kind"
1218 );
1219 }
1220 }
1221
1222 mod output_to_source_tests {
1224 use super::*;
1225
1226 #[test]
1227 fn maps_dist_to_src_with_ts_extension() {
1228 let dir = tempfile::tempdir().expect("create temp dir");
1229 let src = dir.path().join("src");
1230 std::fs::create_dir_all(&src).unwrap();
1231 std::fs::write(src.join("utils.ts"), "export const u = 1;").unwrap();
1232
1233 let result = try_output_to_source_path(dir.path(), "./dist/utils.js");
1234 assert!(result.is_some());
1235 assert!(
1236 result
1237 .unwrap()
1238 .to_string_lossy()
1239 .replace('\\', "/")
1240 .contains("src/utils.ts")
1241 );
1242 }
1243
1244 #[test]
1245 fn returns_none_when_no_source_file_exists() {
1246 let dir = tempfile::tempdir().expect("create temp dir");
1247 let result = try_output_to_source_path(dir.path(), "./dist/missing.js");
1249 assert!(result.is_none());
1250 }
1251
1252 #[test]
1253 fn ignores_non_output_directories() {
1254 let dir = tempfile::tempdir().expect("create temp dir");
1255 let src = dir.path().join("src");
1256 std::fs::create_dir_all(&src).unwrap();
1257 std::fs::write(src.join("foo.ts"), "export const f = 1;").unwrap();
1258
1259 let result = try_output_to_source_path(dir.path(), "./lib/foo.js");
1261 assert!(result.is_none());
1262 }
1263
1264 #[test]
1265 fn maps_nested_output_path_preserving_prefix() {
1266 let dir = tempfile::tempdir().expect("create temp dir");
1267 let modules_src = dir.path().join("modules").join("src");
1268 std::fs::create_dir_all(&modules_src).unwrap();
1269 std::fs::write(modules_src.join("helper.ts"), "export const h = 1;").unwrap();
1270
1271 let result = try_output_to_source_path(dir.path(), "./modules/dist/helper.js");
1272 assert!(result.is_some());
1273 assert!(
1274 result
1275 .unwrap()
1276 .to_string_lossy()
1277 .replace('\\', "/")
1278 .contains("modules/src/helper.ts")
1279 );
1280 }
1281 }
1282
1283 mod source_index_fallback_tests {
1285 use super::*;
1286
1287 #[test]
1288 fn detects_dist_entry_in_output_dir() {
1289 assert!(is_entry_in_output_dir("./dist/esm2022/index.js"));
1290 assert!(is_entry_in_output_dir("dist/index.js"));
1291 assert!(is_entry_in_output_dir("./build/index.js"));
1292 assert!(is_entry_in_output_dir("./out/main.js"));
1293 assert!(is_entry_in_output_dir("./esm/index.js"));
1294 assert!(is_entry_in_output_dir("./cjs/index.js"));
1295 }
1296
1297 #[test]
1298 fn rejects_non_output_entry_paths() {
1299 assert!(!is_entry_in_output_dir("./src/index.ts"));
1300 assert!(!is_entry_in_output_dir("src/main.ts"));
1301 assert!(!is_entry_in_output_dir("./index.js"));
1302 assert!(!is_entry_in_output_dir(""));
1303 }
1304
1305 #[test]
1306 fn rejects_substring_match_for_output_dir() {
1307 assert!(!is_entry_in_output_dir("./distro/index.js"));
1309 assert!(!is_entry_in_output_dir("./build-scripts/run.js"));
1310 }
1311
1312 #[test]
1313 fn finds_src_index_ts() {
1314 let dir = tempfile::tempdir().expect("create temp dir");
1315 let src = dir.path().join("src");
1316 std::fs::create_dir_all(&src).unwrap();
1317 let index_path = src.join("index.ts");
1318 std::fs::write(&index_path, "export const a = 1;").unwrap();
1319
1320 let result = try_source_index_fallback(dir.path());
1321 assert_eq!(result.as_deref(), Some(index_path.as_path()));
1322 }
1323
1324 #[test]
1325 fn finds_src_index_tsx_when_ts_missing() {
1326 let dir = tempfile::tempdir().expect("create temp dir");
1327 let src = dir.path().join("src");
1328 std::fs::create_dir_all(&src).unwrap();
1329 let index_path = src.join("index.tsx");
1330 std::fs::write(&index_path, "export default 1;").unwrap();
1331
1332 let result = try_source_index_fallback(dir.path());
1333 assert_eq!(result.as_deref(), Some(index_path.as_path()));
1334 }
1335
1336 #[test]
1337 fn prefers_src_index_over_root_index() {
1338 let dir = tempfile::tempdir().expect("create temp dir");
1341 let src = dir.path().join("src");
1342 std::fs::create_dir_all(&src).unwrap();
1343 let src_index = src.join("index.ts");
1344 std::fs::write(&src_index, "export const a = 1;").unwrap();
1345 let root_index = dir.path().join("index.ts");
1346 std::fs::write(&root_index, "export const b = 2;").unwrap();
1347
1348 let result = try_source_index_fallback(dir.path());
1349 assert_eq!(result.as_deref(), Some(src_index.as_path()));
1350 }
1351
1352 #[test]
1353 fn falls_back_to_src_main() {
1354 let dir = tempfile::tempdir().expect("create temp dir");
1355 let src = dir.path().join("src");
1356 std::fs::create_dir_all(&src).unwrap();
1357 let main_path = src.join("main.ts");
1358 std::fs::write(&main_path, "export const a = 1;").unwrap();
1359
1360 let result = try_source_index_fallback(dir.path());
1361 assert_eq!(result.as_deref(), Some(main_path.as_path()));
1362 }
1363
1364 #[test]
1365 fn falls_back_to_root_index_when_no_src() {
1366 let dir = tempfile::tempdir().expect("create temp dir");
1367 let index_path = dir.path().join("index.js");
1368 std::fs::write(&index_path, "module.exports = {};").unwrap();
1369
1370 let result = try_source_index_fallback(dir.path());
1371 assert_eq!(result.as_deref(), Some(index_path.as_path()));
1372 }
1373
1374 #[test]
1375 fn returns_none_when_nothing_matches() {
1376 let dir = tempfile::tempdir().expect("create temp dir");
1377 let result = try_source_index_fallback(dir.path());
1378 assert!(result.is_none());
1379 }
1380
1381 #[test]
1382 fn resolve_entry_path_falls_back_to_src_index_for_dist_entry() {
1383 let dir = tempfile::tempdir().expect("create temp dir");
1384 let canonical = dunce::canonicalize(dir.path()).unwrap();
1385
1386 let dist_dir = canonical.join("dist").join("esm2022");
1391 std::fs::create_dir_all(&dist_dir).unwrap();
1392 std::fs::write(dist_dir.join("index.js"), "export const x = 1;").unwrap();
1393
1394 let src = canonical.join("src");
1395 std::fs::create_dir_all(&src).unwrap();
1396 let src_index = src.join("index.ts");
1397 std::fs::write(&src_index, "export const x = 1;").unwrap();
1398
1399 let result = resolve_entry_path(
1400 &canonical,
1401 "./dist/esm2022/index.js",
1402 &canonical,
1403 EntryPointSource::PackageJsonMain,
1404 );
1405 assert!(result.is_some());
1406 let entry = result.unwrap();
1407 assert_eq!(entry.path, src_index);
1408 }
1409
1410 #[test]
1411 fn resolve_entry_path_uses_direct_src_mirror_when_available() {
1412 let dir = tempfile::tempdir().expect("create temp dir");
1415 let canonical = dunce::canonicalize(dir.path()).unwrap();
1416
1417 let src_mirror = canonical.join("src").join("esm2022");
1418 std::fs::create_dir_all(&src_mirror).unwrap();
1419 let mirror_index = src_mirror.join("index.ts");
1420 std::fs::write(&mirror_index, "export const x = 1;").unwrap();
1421
1422 let src_index = canonical.join("src").join("index.ts");
1424 std::fs::write(&src_index, "export const y = 2;").unwrap();
1425
1426 let result = resolve_entry_path(
1427 &canonical,
1428 "./dist/esm2022/index.js",
1429 &canonical,
1430 EntryPointSource::PackageJsonMain,
1431 );
1432 assert_eq!(result.map(|e| e.path), Some(mirror_index));
1433 }
1434 }
1435
1436 mod default_fallback_tests {
1438 use super::*;
1439
1440 #[test]
1441 fn finds_src_index_ts_as_fallback() {
1442 let dir = tempfile::tempdir().expect("create temp dir");
1443 let src = dir.path().join("src");
1444 std::fs::create_dir_all(&src).unwrap();
1445 let index_path = src.join("index.ts");
1446 std::fs::write(&index_path, "export const a = 1;").unwrap();
1447
1448 let files = vec![DiscoveredFile {
1449 id: FileId(0),
1450 path: index_path.clone(),
1451 size_bytes: 20,
1452 }];
1453
1454 let entries = apply_default_fallback(&files, dir.path(), None);
1455 assert_eq!(entries.len(), 1);
1456 assert_eq!(entries[0].path, index_path);
1457 assert!(matches!(entries[0].source, EntryPointSource::DefaultIndex));
1458 }
1459
1460 #[test]
1461 fn finds_root_index_js_as_fallback() {
1462 let dir = tempfile::tempdir().expect("create temp dir");
1463 let index_path = dir.path().join("index.js");
1464 std::fs::write(&index_path, "module.exports = {};").unwrap();
1465
1466 let files = vec![DiscoveredFile {
1467 id: FileId(0),
1468 path: index_path.clone(),
1469 size_bytes: 21,
1470 }];
1471
1472 let entries = apply_default_fallback(&files, dir.path(), None);
1473 assert_eq!(entries.len(), 1);
1474 assert_eq!(entries[0].path, index_path);
1475 }
1476
1477 #[test]
1478 fn returns_empty_when_no_index_file() {
1479 let dir = tempfile::tempdir().expect("create temp dir");
1480 let other_path = dir.path().join("src").join("utils.ts");
1481
1482 let files = vec![DiscoveredFile {
1483 id: FileId(0),
1484 path: other_path,
1485 size_bytes: 10,
1486 }];
1487
1488 let entries = apply_default_fallback(&files, dir.path(), None);
1489 assert!(
1490 entries.is_empty(),
1491 "non-index files should not match default fallback"
1492 );
1493 }
1494
1495 #[test]
1496 fn workspace_filter_restricts_scope() {
1497 let dir = tempfile::tempdir().expect("create temp dir");
1498 let ws_a = dir.path().join("packages").join("a").join("src");
1499 std::fs::create_dir_all(&ws_a).unwrap();
1500 let ws_b = dir.path().join("packages").join("b").join("src");
1501 std::fs::create_dir_all(&ws_b).unwrap();
1502
1503 let index_a = ws_a.join("index.ts");
1504 let index_b = ws_b.join("index.ts");
1505
1506 let files = vec![
1507 DiscoveredFile {
1508 id: FileId(0),
1509 path: index_a.clone(),
1510 size_bytes: 10,
1511 },
1512 DiscoveredFile {
1513 id: FileId(1),
1514 path: index_b,
1515 size_bytes: 10,
1516 },
1517 ];
1518
1519 let ws_root = dir.path().join("packages").join("a");
1521 let entries = apply_default_fallback(&files, &ws_root, Some(&ws_root));
1522 assert_eq!(entries.len(), 1);
1523 assert_eq!(entries[0].path, index_a);
1524 }
1525 }
1526
1527 mod wildcard_entry_tests {
1529 use super::*;
1530
1531 #[test]
1532 fn expands_wildcard_css_entries() {
1533 let dir = tempfile::tempdir().expect("create temp dir");
1536 let themes = dir.path().join("src").join("themes");
1537 std::fs::create_dir_all(&themes).unwrap();
1538 std::fs::write(themes.join("dark.css"), ":root { --bg: #000; }").unwrap();
1539 std::fs::write(themes.join("light.css"), ":root { --bg: #fff; }").unwrap();
1540
1541 let canonical = dunce::canonicalize(dir.path()).unwrap();
1542 let mut entries = Vec::new();
1543 expand_wildcard_entries(dir.path(), "./src/themes/*.css", &canonical, &mut entries);
1544
1545 assert_eq!(entries.len(), 2, "should expand wildcard to 2 CSS files");
1546 let paths: Vec<String> = entries
1547 .iter()
1548 .map(|ep| ep.path.file_name().unwrap().to_string_lossy().to_string())
1549 .collect();
1550 assert!(paths.contains(&"dark.css".to_string()));
1551 assert!(paths.contains(&"light.css".to_string()));
1552 assert!(
1553 entries
1554 .iter()
1555 .all(|ep| matches!(ep.source, EntryPointSource::PackageJsonExports))
1556 );
1557 }
1558
1559 #[test]
1560 fn wildcard_does_not_match_nonexistent_files() {
1561 let dir = tempfile::tempdir().expect("create temp dir");
1562 std::fs::create_dir_all(dir.path().join("src/themes")).unwrap();
1564
1565 let canonical = dunce::canonicalize(dir.path()).unwrap();
1566 let mut entries = Vec::new();
1567 expand_wildcard_entries(dir.path(), "./src/themes/*.css", &canonical, &mut entries);
1568
1569 assert!(
1570 entries.is_empty(),
1571 "should return empty when no files match the wildcard"
1572 );
1573 }
1574
1575 #[test]
1576 fn wildcard_only_matches_specified_extension() {
1577 let dir = tempfile::tempdir().expect("create temp dir");
1579 let themes = dir.path().join("src").join("themes");
1580 std::fs::create_dir_all(&themes).unwrap();
1581 std::fs::write(themes.join("dark.css"), ":root {}").unwrap();
1582 std::fs::write(themes.join("index.ts"), "export {};").unwrap();
1583
1584 let canonical = dunce::canonicalize(dir.path()).unwrap();
1585 let mut entries = Vec::new();
1586 expand_wildcard_entries(dir.path(), "./src/themes/*.css", &canonical, &mut entries);
1587
1588 assert_eq!(entries.len(), 1, "should only match CSS files");
1589 assert!(
1590 entries[0]
1591 .path
1592 .file_name()
1593 .unwrap()
1594 .to_string_lossy()
1595 .ends_with(".css")
1596 );
1597 }
1598 }
1599}