Skip to main content

fallow_engine/
discover.rs

1//! Discovery helpers and types exposed through the engine boundary.
2
3use std::ffi::OsStr;
4use std::path::{Path, PathBuf};
5
6use fallow_config::{PackageJson, ResolvedConfig, WorkspaceInfo};
7pub use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
8
9pub const SOURCE_EXTENSIONS: &[&str] = fallow_core::discover::SOURCE_EXTENSIONS;
10pub const PRODUCTION_EXCLUDE_PATTERNS: &[&str] = fallow_core::discover::PRODUCTION_EXCLUDE_PATTERNS;
11
12/// Entry points grouped by reachability role.
13#[derive(Debug, Clone, Default)]
14pub struct CategorizedEntryPoints {
15    pub all: Vec<EntryPoint>,
16    pub runtime: Vec<EntryPoint>,
17    pub test: Vec<EntryPoint>,
18}
19
20impl CategorizedEntryPoints {
21    #[must_use]
22    pub fn dedup(mut self) -> Self {
23        dedup_entry_paths(&mut self.all);
24        dedup_entry_paths(&mut self.runtime);
25        dedup_entry_paths(&mut self.test);
26        self
27    }
28}
29
30impl From<fallow_core::discover::CategorizedEntryPoints> for CategorizedEntryPoints {
31    fn from(value: fallow_core::discover::CategorizedEntryPoints) -> Self {
32        Self {
33            all: value.all,
34            runtime: value.runtime,
35            test: value.test,
36        }
37    }
38}
39
40fn dedup_entry_paths(entries: &mut Vec<EntryPoint>) {
41    entries.sort_by(|a, b| a.path.cmp(&b.path));
42    entries.dedup_by(|a, b| a.path == b.path);
43}
44
45/// Package-scoped hidden directories that source discovery should traverse.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct HiddenDirScope {
48    root: PathBuf,
49    dirs: Vec<String>,
50}
51
52impl HiddenDirScope {
53    #[must_use]
54    pub const fn new(root: PathBuf, dirs: Vec<String>) -> Self {
55        Self { root, dirs }
56    }
57
58    #[must_use]
59    pub fn root(&self) -> &Path {
60        &self.root
61    }
62
63    #[must_use]
64    pub fn dirs(&self) -> &[String] {
65        &self.dirs
66    }
67}
68
69impl From<fallow_core::discover::HiddenDirScope> for HiddenDirScope {
70    fn from(value: fallow_core::discover::HiddenDirScope) -> Self {
71        Self {
72            root: value.root().to_path_buf(),
73            dirs: value.dirs().to_vec(),
74        }
75    }
76}
77
78impl From<HiddenDirScope> for fallow_core::discover::HiddenDirScope {
79    fn from(value: HiddenDirScope) -> Self {
80        Self::new(value.root, value.dirs)
81    }
82}
83
84/// Reusable engine discovery prelude for one resolved project.
85#[derive(Debug, Clone)]
86pub struct AnalysisDiscovery {
87    inner: fallow_core::AnalysisDiscovery,
88}
89
90impl AnalysisDiscovery {
91    pub(crate) const fn from_core(inner: fallow_core::AnalysisDiscovery) -> Self {
92        Self { inner }
93    }
94
95    pub(crate) const fn as_core(&self) -> &fallow_core::AnalysisDiscovery {
96        &self.inner
97    }
98
99    /// Discovered source files, indexed by stable `FileId` for this session.
100    #[must_use]
101    pub fn files(&self) -> &[DiscoveredFile] {
102        self.inner.files()
103    }
104
105    /// Consume this discovery prelude and return its source file registry.
106    #[must_use]
107    pub fn into_files(self) -> Vec<DiscoveredFile> {
108        self.inner.into_files()
109    }
110}
111
112/// Check if a hidden directory name is on the discovery allowlist.
113#[must_use]
114pub fn is_allowed_hidden_dir(name: &OsStr) -> bool {
115    fallow_core::discover::is_allowed_hidden_dir(name)
116}
117
118/// Collect plugin-derived hidden directory scopes.
119#[must_use]
120pub fn collect_plugin_hidden_dir_scopes(
121    config: &ResolvedConfig,
122    root_pkg: Option<&PackageJson>,
123    workspaces: &[WorkspaceInfo],
124) -> Vec<HiddenDirScope> {
125    fallow_core::discover::collect_plugin_hidden_dir_scopes(config, root_pkg, workspaces)
126        .into_iter()
127        .map(Into::into)
128        .collect()
129}
130
131/// Discover source files for a resolved config.
132#[must_use]
133pub fn discover_files(config: &ResolvedConfig) -> Vec<DiscoveredFile> {
134    fallow_core::discover::discover_files(config)
135}
136
137/// Discover source files with additional package-scoped hidden directories.
138#[must_use]
139pub fn discover_files_with_additional_hidden_dirs(
140    config: &ResolvedConfig,
141    additional_hidden_dir_scopes: &[HiddenDirScope],
142) -> Vec<DiscoveredFile> {
143    let scopes = to_core_hidden_dir_scopes(additional_hidden_dir_scopes);
144    fallow_core::discover::discover_files_with_additional_hidden_dirs(config, &scopes)
145}
146
147/// Discover source files for a resolved config, including plugin scopes.
148#[must_use]
149pub fn discover_files_with_plugin_scopes(config: &ResolvedConfig) -> Vec<DiscoveredFile> {
150    fallow_core::discover::discover_files_with_plugin_scopes(config)
151}
152
153/// Discover configured and inferred entry points.
154#[must_use]
155pub fn discover_entry_points(config: &ResolvedConfig, files: &[DiscoveredFile]) -> Vec<EntryPoint> {
156    fallow_core::discover::discover_entry_points(config, files)
157}
158
159/// Discover entry points for a workspace package.
160#[must_use]
161pub fn discover_workspace_entry_points(
162    ws_root: &Path,
163    config: &ResolvedConfig,
164    all_files: &[DiscoveredFile],
165) -> Vec<EntryPoint> {
166    fallow_core::discover::discover_workspace_entry_points(ws_root, config, all_files)
167}
168
169/// Discover entry points from plugin results.
170#[must_use]
171pub fn discover_plugin_entry_points(
172    plugin_result: &crate::plugins::AggregatedPluginResult,
173    config: &ResolvedConfig,
174    files: &[DiscoveredFile],
175) -> Vec<EntryPoint> {
176    fallow_core::discover::discover_plugin_entry_points(plugin_result.as_core(), config, files)
177}
178
179fn to_core_hidden_dir_scopes(
180    scopes: &[HiddenDirScope],
181) -> Vec<fallow_core::discover::HiddenDirScope> {
182    scopes.iter().cloned().map(Into::into).collect()
183}
184
185#[cfg(test)]
186mod tests {
187    use std::path::PathBuf;
188
189    use super::{CategorizedEntryPoints, EntryPoint, EntryPointSource, HiddenDirScope};
190
191    #[test]
192    fn hidden_dir_scope_round_trips_through_core() {
193        let scope = HiddenDirScope::new(PathBuf::from("/repo/packages/app"), vec![".next".into()]);
194
195        let core: fallow_core::discover::HiddenDirScope = scope.clone().into();
196        let engine: HiddenDirScope = core.into();
197
198        assert_eq!(engine, scope);
199        assert_eq!(engine.root(), scope.root());
200        assert_eq!(engine.dirs(), scope.dirs());
201    }
202
203    #[test]
204    fn categorized_entry_points_converts_from_core() {
205        let entry = EntryPoint {
206            path: PathBuf::from("/repo/src/index.ts"),
207            source: EntryPointSource::DefaultIndex,
208        };
209        let mut core = fallow_core::discover::CategorizedEntryPoints::default();
210        core.push_runtime(entry.clone());
211
212        let engine: CategorizedEntryPoints = core.into();
213
214        assert_eq!(engine.all.len(), 1);
215        assert_eq!(engine.runtime.len(), 1);
216        assert_eq!(engine.test.len(), 0);
217        assert_eq!(engine.all[0].path, entry.path);
218    }
219}