Skip to main content

fallow_engine/
list_inventory.rs

1//! Engine-owned inventory helpers for list-style project metadata.
2
3use std::path::{Path, PathBuf};
4
5use fallow_config::{ResolvedConfig, WorkspaceInfo};
6
7use crate::{
8    discover::{DiscoveredFile, EntryPoint},
9    plugins::{AggregatedPluginResult, PluginRegistry, registry::PluginRegexValidationError},
10};
11
12/// Error raised while assembling list inventory.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ListInventoryError {
15    /// One or more plugin regexes failed validation.
16    PluginRegex(Vec<PluginRegexValidationError>),
17}
18
19/// Collect active plugins from the root package and every workspace package.
20///
21/// Missing package manifests are ignored, matching the historical list-command
22/// behavior. Deno members are loaded via `deno.json` the same way analysis does.
23///
24/// # Errors
25///
26/// Returns plugin regex validation errors from user-authored plugin settings.
27pub fn collect_active_plugins(
28    root: &Path,
29    config: &ResolvedConfig,
30    discovered: &[DiscoveredFile],
31    workspaces: &[WorkspaceInfo],
32) -> Result<AggregatedPluginResult, ListInventoryError> {
33    let file_paths = discovered
34        .iter()
35        .map(|file| file.path.clone())
36        .collect::<Vec<_>>();
37    let registry = PluginRegistry::new(config.external_plugins.clone());
38    let mut result = run_package_plugins(&registry, root, &file_paths)?.unwrap_or_default();
39
40    for workspace in workspaces {
41        let Some(workspace_result) = run_package_plugins(&registry, &workspace.root, &file_paths)?
42        else {
43            continue;
44        };
45        result.merge_active_plugins_from(&workspace_result);
46    }
47
48    Ok(result)
49}
50
51/// Collect root, workspace, and plugin entry points in one engine-owned pass.
52#[must_use]
53pub fn collect_entry_points(
54    config: &ResolvedConfig,
55    discovered: &[DiscoveredFile],
56    workspaces: &[WorkspaceInfo],
57    plugin_result: Option<&AggregatedPluginResult>,
58) -> Vec<EntryPoint> {
59    let mut entries = crate::discover::discover_entry_points(config, discovered);
60    for workspace in workspaces {
61        entries.extend(crate::discover::discover_workspace_entry_points(
62            &workspace.root,
63            config,
64            discovered,
65        ));
66    }
67    if let Some(plugin_result) = plugin_result {
68        entries.extend(crate::discover::discover_plugin_entry_points(
69            plugin_result,
70            config,
71            discovered,
72        ));
73    }
74    entries
75}
76
77fn run_package_plugins(
78    registry: &PluginRegistry,
79    package_root: &Path,
80    file_paths: &[PathBuf],
81) -> Result<Option<AggregatedPluginResult>, ListInventoryError> {
82    let Some(package) = fallow_config::load_dir_package_json(package_root) else {
83        return Ok(None);
84    };
85    registry
86        .try_run(&package, package_root, file_paths)
87        .map(Some)
88        .map_err(ListInventoryError::PluginRegex)
89}
90
91#[cfg(test)]
92mod tests {
93    use std::path::Path;
94
95    use fallow_config::{FallowConfig, WorkspaceInfo};
96    use fallow_types::output_format::OutputFormat;
97
98    use super::*;
99    use crate::discover::{EntryPointSource, FileId};
100
101    #[test]
102    fn entry_points_include_root_and_workspace_entries() {
103        let temp = tempfile::tempdir().expect("tempdir");
104        let root = temp.path();
105        let config = FallowConfig::default().resolve(
106            root.to_path_buf(),
107            OutputFormat::Json,
108            1,
109            false,
110            true,
111            None,
112        );
113        let workspace = WorkspaceInfo {
114            root: root.join("packages/web"),
115            name: "web".to_owned(),
116            is_internal_dependency: false,
117        };
118        let discovered = vec![
119            DiscoveredFile {
120                id: FileId(0),
121                path: root.join("src/main.ts"),
122                size_bytes: 0,
123            },
124            DiscoveredFile {
125                id: FileId(1),
126                path: root.join("packages/web/src/index.ts"),
127                size_bytes: 0,
128            },
129        ];
130
131        let entries = collect_entry_points(&config, &discovered, &[workspace], None);
132
133        assert!(
134            entries
135                .iter()
136                .any(|entry| entry.path.ends_with("src/main.ts"))
137        );
138        assert!(
139            entries
140                .iter()
141                .any(|entry| entry.path.ends_with("packages/web/src/index.ts"))
142        );
143    }
144
145    #[test]
146    fn active_plugins_ignores_missing_package_manifests() {
147        let config = FallowConfig::default().resolve(
148            Path::new("/missing-project").to_path_buf(),
149            OutputFormat::Json,
150            1,
151            false,
152            true,
153            None,
154        );
155        let result = collect_active_plugins(Path::new("/missing-project"), &config, &[], &[])
156            .expect("missing package should not fail");
157
158        assert!(result.active_plugins().is_empty());
159    }
160
161    #[test]
162    fn entry_points_accept_plugin_result() {
163        let config = FallowConfig::default().resolve(
164            Path::new("/project").to_path_buf(),
165            OutputFormat::Json,
166            1,
167            false,
168            true,
169            None,
170        );
171        let discovered = Vec::new();
172
173        let entries = collect_entry_points(&config, &discovered, &[], None);
174
175        assert!(
176            entries
177                .iter()
178                .all(|entry| !matches!(entry.source, EntryPointSource::Plugin { .. }))
179        );
180    }
181}