whisker_dev_server/
workspace.rs1use anyhow::{Context, Result};
24use cargo_metadata::MetadataCommand;
25use std::collections::HashSet;
26use std::path::{Path, PathBuf};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct PathDepCrate {
31 pub crate_name: String,
34 pub src_dir: PathBuf,
38}
39
40pub fn discover_path_deps(manifest_path: &Path, app_package: &str) -> Result<Vec<PathDepCrate>> {
50 let metadata = MetadataCommand::new()
51 .manifest_path(manifest_path)
52 .exec()
53 .with_context(|| {
54 format!(
55 "cargo metadata failed for {} (package: {app_package})",
56 manifest_path.display(),
57 )
58 })?;
59
60 let resolve = metadata
61 .resolve
62 .as_ref()
63 .context("cargo metadata returned no resolve graph")?;
64 let root_id = resolve
65 .root
66 .as_ref()
67 .cloned()
68 .or_else(|| {
69 metadata
70 .packages
71 .iter()
72 .find(|p| p.name == app_package)
73 .map(|p| p.id.clone())
74 })
75 .with_context(|| format!("cargo package `{app_package}` not found in the workspace"))?;
76
77 let mut out: Vec<PathDepCrate> = Vec::new();
78 let mut visit: Vec<&cargo_metadata::PackageId> = vec![&root_id];
79 let mut seen: HashSet<&cargo_metadata::PackageId> = HashSet::new();
80
81 while let Some(pkg_id) = visit.pop() {
82 if !seen.insert(pkg_id) {
83 continue;
84 }
85 let Some(pkg) = metadata.packages.iter().find(|p| &p.id == pkg_id) else {
86 continue;
87 };
88 if pkg.source.is_some() {
91 continue;
92 }
93 if let Some(manifest_dir) = pkg.manifest_path.parent() {
94 let src_dir = manifest_dir.join("src");
95 out.push(PathDepCrate {
96 crate_name: pkg.name.replace('-', "_"),
97 src_dir: src_dir.into(),
98 });
99 }
100 if let Some(node) = resolve.nodes.iter().find(|n| &n.id == pkg_id) {
101 for dep in &node.deps {
102 visit.push(&dep.pkg);
103 }
104 }
105 }
106 Ok(out)
107}
108
109pub fn identify_crate_for_paths(paths: &[PathBuf], crates: &[PathDepCrate]) -> Option<String> {
116 let mut found: Option<&str> = None;
117 for p in paths {
118 let hit = best_crate_for(p, crates)?;
119 match found {
120 None => found = Some(hit),
121 Some(prev) if prev != hit => return None,
122 _ => {}
123 }
124 }
125 found.map(str::to_owned)
126}
127
128fn best_crate_for<'a>(path: &Path, crates: &'a [PathDepCrate]) -> Option<&'a str> {
132 let mut best: Option<(&str, usize)> = None;
133 for c in crates {
134 if path.starts_with(&c.src_dir) {
135 let depth = c.src_dir.components().count();
136 if best.map(|(_, d)| depth > d).unwrap_or(true) {
137 best = Some((&c.crate_name, depth));
138 }
139 }
140 }
141 best.map(|(n, _)| n)
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn cr(name: &str, dir: &str) -> PathDepCrate {
149 PathDepCrate {
150 crate_name: name.into(),
151 src_dir: PathBuf::from(dir),
152 }
153 }
154
155 #[test]
156 fn identify_returns_the_matching_crate() {
157 let crates = vec![
158 cr("podcast", "/ws/examples/podcast/src"),
159 cr(
160 "podcast_ui_kit",
161 "/ws/examples/podcast/crates/podcast-ui-kit/src",
162 ),
163 ];
164 let paths = vec![PathBuf::from(
165 "/ws/examples/podcast/crates/podcast-ui-kit/src/top_nav.rs",
166 )];
167 assert_eq!(
168 identify_crate_for_paths(&paths, &crates),
169 Some("podcast_ui_kit".into())
170 );
171 }
172
173 #[test]
174 fn identify_returns_none_when_paths_span_multiple_crates() {
175 let crates = vec![
176 cr("podcast", "/ws/examples/podcast/src"),
177 cr(
178 "podcast_ui_kit",
179 "/ws/examples/podcast/crates/podcast-ui-kit/src",
180 ),
181 ];
182 let paths = vec![
183 PathBuf::from("/ws/examples/podcast/src/lib.rs"),
184 PathBuf::from("/ws/examples/podcast/crates/podcast-ui-kit/src/top_nav.rs"),
185 ];
186 assert_eq!(identify_crate_for_paths(&paths, &crates), None);
187 }
188
189 #[test]
190 fn identify_returns_none_when_no_crate_matches() {
191 let crates = vec![cr("podcast", "/ws/examples/podcast/src")];
192 let paths = vec![PathBuf::from("/some/unrelated/path/foo.rs")];
193 assert_eq!(identify_crate_for_paths(&paths, &crates), None);
194 }
195
196 #[test]
197 fn identify_picks_the_deeper_match_when_src_dirs_nest() {
198 let crates = vec![
202 cr("outer", "/ws/foo/src"),
203 cr("inner", "/ws/foo/src/inner_pkg/src"),
204 ];
205 let paths = vec![PathBuf::from("/ws/foo/src/inner_pkg/src/lib.rs")];
206 assert_eq!(
207 identify_crate_for_paths(&paths, &crates),
208 Some("inner".into())
209 );
210 }
211
212 #[test]
213 fn identify_handles_single_crate_batch() {
214 let crates = vec![cr("podcast", "/ws/podcast/src")];
215 let paths = vec![
216 PathBuf::from("/ws/podcast/src/lib.rs"),
217 PathBuf::from("/ws/podcast/src/main.rs"),
218 ];
219 assert_eq!(
220 identify_crate_for_paths(&paths, &crates),
221 Some("podcast".into())
222 );
223 }
224}