provenant/parsers/
npm_workspace.rs1use crate::models::PackageData;
25use crate::models::{DatasourceId, PackageType};
26use crate::parser_warn as warn;
27use crate::parsers::utils::{MAX_ITERATION_COUNT, read_file_to_string, truncate_field};
28use std::path::Path;
29use yaml_serde::Value;
30
31use super::PackageParser;
32use super::metadata::ParserMetadata;
33
34pub struct NpmWorkspaceParser;
38
39impl PackageParser for NpmWorkspaceParser {
40 const PACKAGE_TYPE: PackageType = PackageType::Npm;
41
42 fn metadata() -> Vec<ParserMetadata> {
43 vec![ParserMetadata {
44 description: "pnpm workspace yaml file",
45 file_patterns: &["**/pnpm-workspace.yaml"],
46 package_type: "npm",
47 primary_language: "JavaScript",
48 documentation_url: Some("https://pnpm.io/pnpm-workspace_yaml"),
49 }]
50 }
51
52 fn is_match(path: &Path) -> bool {
53 path.file_name()
54 .and_then(|name| name.to_str())
55 .map(|name| name == "pnpm-workspace.yaml")
56 .unwrap_or(false)
57 }
58
59 fn extract_packages(path: &Path) -> Vec<PackageData> {
60 let content = match read_file_to_string(path, None) {
61 Ok(content) => content,
62 Err(e) => {
63 warn!("Failed to read npm workspace file at {:?}: {}", path, e);
64 return vec![default_package_data()];
65 }
66 };
67
68 let workspace_data: Value = match yaml_serde::from_str(&content) {
69 Ok(data) => data,
70 Err(e) => {
71 crate::parser_warn!("Failed to parse npm workspace file at {:?}: {}", path, e);
72 return vec![default_package_data()];
73 }
74 };
75
76 vec![parse_workspace_file(&workspace_data)]
77 }
78}
79
80fn default_package_data() -> PackageData {
82 PackageData {
83 package_type: Some(NpmWorkspaceParser::PACKAGE_TYPE),
84 datasource_id: Some(DatasourceId::PnpmWorkspaceYaml),
85 ..Default::default()
86 }
87}
88
89fn parse_workspace_file(workspace_data: &Value) -> PackageData {
91 let workspaces = workspace_data.get("packages").and_then(|v| v.as_sequence());
93
94 match workspaces {
95 Some(workspace_patterns) => {
96 let workspaces_vec: Vec<String> = workspace_patterns
97 .iter()
98 .take(MAX_ITERATION_COUNT)
99 .filter_map(|v| v.as_str())
100 .map(|s| truncate_field(s.to_string()))
101 .collect();
102
103 PackageData {
104 package_type: Some(NpmWorkspaceParser::PACKAGE_TYPE),
105 extra_data: if workspaces_vec.is_empty() {
106 None
107 } else {
108 let mut extra = std::collections::HashMap::new();
109 extra.insert(
110 "datasource_id".to_string(),
111 serde_json::Value::String("pnpm_workspace_yaml".to_string()),
112 );
113 extra.insert(
114 "workspaces".to_string(),
115 serde_json::Value::Array(
116 workspaces_vec
117 .into_iter()
118 .map(serde_json::Value::String)
119 .collect(),
120 ),
121 );
122 Some(extra)
123 },
124 ..default_package_data()
125 }
126 }
127 None => {
128 PackageData {
130 package_type: Some(NpmWorkspaceParser::PACKAGE_TYPE),
131 extra_data: {
132 let mut extra = std::collections::HashMap::new();
133 extra.insert(
134 "datasource_id".to_string(),
135 serde_json::Value::String("pnpm_workspace_yaml".to_string()),
136 );
137 Some(extra)
138 },
139 ..default_package_data()
140 }
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_is_match() {
151 assert!(NpmWorkspaceParser::is_match(Path::new(
152 "pnpm-workspace.yaml"
153 )));
154 assert!(!NpmWorkspaceParser::is_match(Path::new("package.json")));
155 assert!(!NpmWorkspaceParser::is_match(Path::new("pnpm-lock.yaml")));
156 assert!(!NpmWorkspaceParser::is_match(Path::new("README.md")));
157 }
158
159 #[test]
160 fn test_parse_workspace_with_single_package() {
161 let yaml_content = r#"
162packages:
163 - "packages/*"
164"#;
165
166 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
167 let result = parse_workspace_file(&workspace_data);
168
169 assert_eq!(result.package_type, Some(PackageType::Npm));
170
171 let extra_data = result.extra_data.unwrap();
172 assert_eq!(
173 extra_data.get("datasource_id").unwrap().as_str().unwrap(),
174 "pnpm_workspace_yaml"
175 );
176 let workspaces = extra_data.get("workspaces").unwrap().as_array().unwrap();
177 assert_eq!(workspaces.len(), 1);
178 assert_eq!(workspaces[0], "packages/*");
179 }
180
181 #[test]
182 fn test_parse_workspace_with_multiple_packages() {
183 let yaml_content = r#"
184packages:
185 - "packages/*"
186 - "apps/*"
187 - "tools/*"
188"#;
189
190 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
191 let result = parse_workspace_file(&workspace_data);
192
193 let extra_data = result.extra_data.unwrap();
194 let workspaces = extra_data.get("workspaces").unwrap().as_array().unwrap();
195 assert_eq!(workspaces.len(), 3);
196 assert_eq!(workspaces[0], "packages/*");
197 assert_eq!(workspaces[1], "apps/*");
198 assert_eq!(workspaces[2], "tools/*");
199 }
200
201 #[test]
202 fn test_parse_workspace_with_wildcard_pattern() {
203 let yaml_content = r#"
204packages:
205 - "*"
206"#;
207
208 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
209 let result = parse_workspace_file(&workspace_data);
210
211 let extra_data = result.extra_data.unwrap();
212 let workspaces = extra_data.get("workspaces").unwrap().as_array().unwrap();
213 assert_eq!(workspaces.len(), 1);
214 assert_eq!(workspaces[0], "*");
215 }
216
217 #[test]
218 fn test_parse_workspace_with_negated_pattern() {
219 let yaml_content = r#"
220packages:
221 - "packages/*"
222 - "!packages/dont-scan-me"
223"#;
224
225 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
226 let result = parse_workspace_file(&workspace_data);
227
228 let extra_data = result.extra_data.unwrap();
229 let workspaces = extra_data.get("workspaces").unwrap().as_array().unwrap();
230 assert_eq!(workspaces.len(), 2);
231 assert_eq!(workspaces[0], "packages/*");
232 assert_eq!(workspaces[1], "!packages/dont-scan-me");
233 }
234
235 #[test]
236 fn test_parse_workspace_with_depth_pattern() {
237 let yaml_content = r#"
238packages:
239 - "**/components/*"
240"#;
241
242 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
243 let result = parse_workspace_file(&workspace_data);
244
245 let extra_data = result.extra_data.unwrap();
246 let workspaces = extra_data.get("workspaces").unwrap().as_array().unwrap();
247 assert_eq!(workspaces.len(), 1);
248 assert_eq!(workspaces[0], "**/components/*");
249 }
250
251 #[test]
252 fn test_parse_workspace_with_no_packages() {
253 let yaml_content = r#"
254name: my-workspace
255"#;
256
257 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
258 let result = parse_workspace_file(&workspace_data);
259
260 assert_eq!(result.package_type, Some(PackageType::Npm));
261 assert!(result.extra_data.is_some());
262 let extra_data = result.extra_data.unwrap();
263 assert_eq!(
264 extra_data.get("datasource_id").unwrap().as_str().unwrap(),
265 "pnpm_workspace_yaml"
266 );
267 assert!(!extra_data.contains_key("workspaces"));
268 }
269
270 #[test]
271 fn test_parse_workspace_with_empty_packages_array() {
272 let yaml_content = r#"
273packages: []
274"#;
275
276 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
277 let result = parse_workspace_file(&workspace_data);
278
279 assert_eq!(result.package_type, Some(PackageType::Npm));
280 assert!(
281 result.extra_data.is_none() || !result.extra_data.unwrap().contains_key("workspaces")
282 );
283 }
284
285 #[test]
286 fn test_default_package_data() {
287 let result = default_package_data();
288
289 assert_eq!(result.package_type, Some(PackageType::Npm));
290 assert!(result.name.is_none());
291 assert!(result.version.is_none());
292 assert!(result.extra_data.is_none());
293 }
294}