Skip to main content

fallow_core/discover/
mod.rs

1mod entry_points;
2mod infrastructure;
3mod parse_scripts;
4mod walk;
5
6use std::path::Path;
7
8use fallow_config::{PackageJson, ResolvedConfig};
9
10// Re-export types from fallow-types
11pub use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
12
13// Re-export public functions — preserves the existing `crate::discover::*` API
14pub use entry_points::{
15    CategorizedEntryPoints, compile_glob_set, discover_dynamically_loaded_entry_points,
16    discover_entry_points, discover_plugin_entry_point_sets, discover_plugin_entry_points,
17    discover_workspace_entry_points,
18};
19pub(crate) use entry_points::{
20    EntryPointDiscovery, discover_entry_points_with_warnings_from_pkg,
21    discover_workspace_entry_points_with_warnings_from_pkg, warn_skipped_entry_summary,
22};
23pub use infrastructure::discover_infrastructure_entry_points;
24pub use walk::{
25    HiddenDirScope, PRODUCTION_EXCLUDE_PATTERNS, SOURCE_EXTENSIONS, discover_files,
26    discover_files_with_additional_hidden_dirs,
27};
28
29/// Collect package-scoped hidden directory traversal rules for active plugins.
30///
31/// Source discovery runs before full plugin execution, so this consults
32/// package-activation checks and static plugin metadata only. Callers that have
33/// already loaded the root `package.json` and discovered workspaces should pass
34/// them in to avoid redoing the work; standalone CLI command paths can use
35/// [`discover_files_with_plugin_scopes`] instead.
36#[must_use]
37pub fn collect_plugin_hidden_dir_scopes(
38    config: &ResolvedConfig,
39    root_pkg: Option<&PackageJson>,
40    workspaces: &[fallow_config::WorkspaceInfo],
41) -> Vec<HiddenDirScope> {
42    let registry = crate::plugins::PluginRegistry::new(config.external_plugins.clone());
43    let mut scopes = Vec::new();
44
45    if let Some(pkg) = root_pkg {
46        push_plugin_hidden_dir_scope(&mut scopes, &registry, pkg, &config.root);
47    }
48
49    for ws in workspaces {
50        if let Ok(pkg) = PackageJson::load(&ws.root.join("package.json")) {
51            push_plugin_hidden_dir_scope(&mut scopes, &registry, &pkg, &ws.root);
52        }
53    }
54
55    scopes
56}
57
58fn push_plugin_hidden_dir_scope(
59    scopes: &mut Vec<HiddenDirScope>,
60    registry: &crate::plugins::PluginRegistry,
61    pkg: &PackageJson,
62    root: &Path,
63) {
64    let dirs = registry.discovery_hidden_dirs(pkg, root);
65    if !dirs.is_empty() {
66        scopes.push(HiddenDirScope::new(root.to_path_buf(), dirs));
67    }
68}
69
70/// Discover files with plugin-aware hidden directory traversal.
71///
72/// Convenience wrapper for command paths (list, dupes, health, flags, coverage)
73/// that don't already have workspaces / root `package.json` on hand. Internally
74/// loads the root `package.json` and discovers workspaces so plugin-contributed
75/// hidden directories (e.g. React Router's `.client` / `.server` folders) are
76/// traversed consistently across every command.
77#[must_use]
78pub fn discover_files_with_plugin_scopes(config: &ResolvedConfig) -> Vec<DiscoveredFile> {
79    let root_pkg = PackageJson::load(&config.root.join("package.json")).ok();
80    let workspaces = fallow_config::discover_workspaces(&config.root);
81    let scopes = collect_plugin_hidden_dir_scopes(config, root_pkg.as_ref(), &workspaces);
82    discover_files_with_additional_hidden_dirs(config, &scopes)
83}
84
85/// Hidden (dot-prefixed) directories that should be included in file discovery.
86///
87/// Most hidden directories (`.git`, `.cache`, etc.) should be skipped, but certain
88/// convention directories contain source or config files that fallow needs to see:
89/// - `.storybook` — Storybook configuration (the Storybook plugin depends on this)
90/// - `.vitepress` — VitePress configuration and theme files
91/// - `.well-known` — Standard web convention directory
92/// - `.changeset` — Changesets configuration
93/// - `.github` — GitHub workflows and CI scripts
94const ALLOWED_HIDDEN_DIRS: &[&str] = &[
95    ".storybook",
96    ".vitepress",
97    ".well-known",
98    ".changeset",
99    ".github",
100];
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    // ── ALLOWED_HIDDEN_DIRS exhaustiveness ───────────────────────────
107
108    #[test]
109    fn allowed_hidden_dirs_count() {
110        // Guard: if a new dir is added, add a test for it
111        assert_eq!(
112            ALLOWED_HIDDEN_DIRS.len(),
113            5,
114            "update tests when adding new allowed hidden dirs"
115        );
116    }
117
118    #[test]
119    fn allowed_hidden_dirs_all_start_with_dot() {
120        for dir in ALLOWED_HIDDEN_DIRS {
121            assert!(
122                dir.starts_with('.'),
123                "allowed hidden dir '{dir}' must start with '.'"
124            );
125        }
126    }
127
128    #[test]
129    fn allowed_hidden_dirs_no_duplicates() {
130        let mut seen = rustc_hash::FxHashSet::default();
131        for dir in ALLOWED_HIDDEN_DIRS {
132            assert!(seen.insert(*dir), "duplicate allowed hidden dir: {dir}");
133        }
134    }
135
136    #[test]
137    fn allowed_hidden_dirs_no_trailing_slash() {
138        for dir in ALLOWED_HIDDEN_DIRS {
139            assert!(
140                !dir.ends_with('/'),
141                "allowed hidden dir '{dir}' should not have trailing slash"
142            );
143        }
144    }
145
146    // ── Re-export smoke tests ───────────────────────────────────────
147
148    #[test]
149    fn file_id_re_exported() {
150        // Verify the re-export works by constructing a FileId through the discover module
151        let id = FileId(42);
152        assert_eq!(id.0, 42);
153    }
154
155    #[test]
156    fn source_extensions_re_exported() {
157        assert!(SOURCE_EXTENSIONS.contains(&"ts"));
158        assert!(SOURCE_EXTENSIONS.contains(&"tsx"));
159    }
160
161    #[test]
162    fn compile_glob_set_re_exported() {
163        let result = compile_glob_set(&["**/*.ts".to_string()]);
164        assert!(result.is_some());
165    }
166}