fallow_engine/
list_inventory.rs1use std::path::{Path, PathBuf};
4
5use fallow_config::{PackageJson, ResolvedConfig, WorkspaceInfo};
6
7use crate::{
8 discover::{DiscoveredFile, EntryPoint},
9 plugins::{AggregatedPluginResult, PluginRegistry, registry::PluginRegexValidationError},
10};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ListInventoryError {
15 PluginRegex(Vec<PluginRegexValidationError>),
17}
18
19pub 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(®istry, &root.join("package.json"), root, &file_paths)?
39 .unwrap_or_default();
40
41 for workspace in workspaces {
42 let Some(workspace_result) = run_package_plugins(
43 ®istry,
44 &workspace.root.join("package.json"),
45 &workspace.root,
46 &file_paths,
47 )?
48 else {
49 continue;
50 };
51 result.merge_active_plugins_from(&workspace_result);
52 }
53
54 Ok(result)
55}
56
57#[must_use]
59pub fn collect_entry_points(
60 config: &ResolvedConfig,
61 discovered: &[DiscoveredFile],
62 workspaces: &[WorkspaceInfo],
63 plugin_result: Option<&AggregatedPluginResult>,
64) -> Vec<EntryPoint> {
65 let mut entries = crate::discover::discover_entry_points(config, discovered);
66 for workspace in workspaces {
67 entries.extend(crate::discover::discover_workspace_entry_points(
68 &workspace.root,
69 config,
70 discovered,
71 ));
72 }
73 if let Some(plugin_result) = plugin_result {
74 entries.extend(crate::discover::discover_plugin_entry_points(
75 plugin_result,
76 config,
77 discovered,
78 ));
79 }
80 entries
81}
82
83fn run_package_plugins(
84 registry: &PluginRegistry,
85 package_path: &Path,
86 root: &Path,
87 file_paths: &[PathBuf],
88) -> Result<Option<AggregatedPluginResult>, ListInventoryError> {
89 let Ok(package) = PackageJson::load(package_path) else {
90 return Ok(None);
91 };
92 registry
93 .try_run(&package, root, file_paths)
94 .map(Some)
95 .map_err(ListInventoryError::PluginRegex)
96}
97
98#[cfg(test)]
99mod tests {
100 use std::path::Path;
101
102 use fallow_config::{FallowConfig, WorkspaceInfo};
103 use fallow_types::output_format::OutputFormat;
104
105 use super::*;
106 use crate::discover::{EntryPointSource, FileId};
107
108 #[test]
109 fn entry_points_include_root_and_workspace_entries() {
110 let temp = tempfile::tempdir().expect("tempdir");
111 let root = temp.path();
112 let config = FallowConfig::default().resolve(
113 root.to_path_buf(),
114 OutputFormat::Json,
115 1,
116 false,
117 true,
118 None,
119 );
120 let workspace = WorkspaceInfo {
121 root: root.join("packages/web"),
122 name: "web".to_owned(),
123 is_internal_dependency: false,
124 };
125 let discovered = vec![
126 DiscoveredFile {
127 id: FileId(0),
128 path: root.join("src/main.ts"),
129 size_bytes: 0,
130 },
131 DiscoveredFile {
132 id: FileId(1),
133 path: root.join("packages/web/src/index.ts"),
134 size_bytes: 0,
135 },
136 ];
137
138 let entries = collect_entry_points(&config, &discovered, &[workspace], None);
139
140 assert!(
141 entries
142 .iter()
143 .any(|entry| entry.path.ends_with("src/main.ts"))
144 );
145 assert!(
146 entries
147 .iter()
148 .any(|entry| entry.path.ends_with("packages/web/src/index.ts"))
149 );
150 }
151
152 #[test]
153 fn active_plugins_ignores_missing_package_manifests() {
154 let config = FallowConfig::default().resolve(
155 Path::new("/missing-project").to_path_buf(),
156 OutputFormat::Json,
157 1,
158 false,
159 true,
160 None,
161 );
162 let result = collect_active_plugins(Path::new("/missing-project"), &config, &[], &[])
163 .expect("missing package should not fail");
164
165 assert!(result.active_plugins().is_empty());
166 }
167
168 #[test]
169 fn entry_points_accept_plugin_result() {
170 let config = FallowConfig::default().resolve(
171 Path::new("/project").to_path_buf(),
172 OutputFormat::Json,
173 1,
174 false,
175 true,
176 None,
177 );
178 let discovered = Vec::new();
179
180 let entries = collect_entry_points(&config, &discovered, &[], None);
181
182 assert!(
183 entries
184 .iter()
185 .all(|entry| !matches!(entry.source, EntryPointSource::Plugin { .. }))
186 );
187 }
188}