provenant/parsers/
npm_workspace.rs1use crate::models::PackageData;
25use crate::models::{DatasourceId, PackageType};
26use crate::parser_warn as warn;
27use crate::parsers::utils::{capped_iteration_limit, 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 limit = capped_iteration_limit(
97 workspace_patterns.len(),
98 "pnpm workspace: packages patterns",
99 );
100 let workspaces_vec: Vec<String> = workspace_patterns
101 .iter()
102 .take(limit)
103 .filter_map(|v| v.as_str())
104 .map(|s| truncate_field(s.to_string()))
105 .collect();
106
107 PackageData {
108 package_type: Some(NpmWorkspaceParser::PACKAGE_TYPE),
109 extra_data: if workspaces_vec.is_empty() {
110 None
111 } else {
112 let mut extra = std::collections::HashMap::new();
113 extra.insert(
114 "datasource_id".to_string(),
115 serde_json::Value::String("pnpm_workspace_yaml".to_string()),
116 );
117 extra.insert(
118 "workspaces".to_string(),
119 serde_json::Value::Array(
120 workspaces_vec
121 .into_iter()
122 .map(serde_json::Value::String)
123 .collect(),
124 ),
125 );
126 Some(extra)
127 },
128 ..default_package_data()
129 }
130 }
131 None => {
132 PackageData {
134 package_type: Some(NpmWorkspaceParser::PACKAGE_TYPE),
135 extra_data: {
136 let mut extra = std::collections::HashMap::new();
137 extra.insert(
138 "datasource_id".to_string(),
139 serde_json::Value::String("pnpm_workspace_yaml".to_string()),
140 );
141 Some(extra)
142 },
143 ..default_package_data()
144 }
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_is_match() {
155 assert!(NpmWorkspaceParser::is_match(Path::new(
156 "pnpm-workspace.yaml"
157 )));
158 assert!(!NpmWorkspaceParser::is_match(Path::new("package.json")));
159 assert!(!NpmWorkspaceParser::is_match(Path::new("pnpm-lock.yaml")));
160 assert!(!NpmWorkspaceParser::is_match(Path::new("README.md")));
161 }
162
163 #[test]
164 fn test_parse_workspace_with_single_package() {
165 let yaml_content = r#"
166packages:
167 - "packages/*"
168"#;
169
170 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
171 let result = parse_workspace_file(&workspace_data);
172
173 assert_eq!(result.package_type, Some(PackageType::Npm));
174
175 let extra_data = result.extra_data.unwrap();
176 assert_eq!(
177 extra_data.get("datasource_id").unwrap().as_str().unwrap(),
178 "pnpm_workspace_yaml"
179 );
180 let workspaces = extra_data.get("workspaces").unwrap().as_array().unwrap();
181 assert_eq!(workspaces.len(), 1);
182 assert_eq!(workspaces[0], "packages/*");
183 }
184
185 #[test]
186 fn test_parse_workspace_with_multiple_packages() {
187 let yaml_content = r#"
188packages:
189 - "packages/*"
190 - "apps/*"
191 - "tools/*"
192"#;
193
194 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
195 let result = parse_workspace_file(&workspace_data);
196
197 let extra_data = result.extra_data.unwrap();
198 let workspaces = extra_data.get("workspaces").unwrap().as_array().unwrap();
199 assert_eq!(workspaces.len(), 3);
200 assert_eq!(workspaces[0], "packages/*");
201 assert_eq!(workspaces[1], "apps/*");
202 assert_eq!(workspaces[2], "tools/*");
203 }
204
205 #[test]
206 fn test_parse_workspace_with_wildcard_pattern() {
207 let yaml_content = r#"
208packages:
209 - "*"
210"#;
211
212 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
213 let result = parse_workspace_file(&workspace_data);
214
215 let extra_data = result.extra_data.unwrap();
216 let workspaces = extra_data.get("workspaces").unwrap().as_array().unwrap();
217 assert_eq!(workspaces.len(), 1);
218 assert_eq!(workspaces[0], "*");
219 }
220
221 #[test]
222 fn test_parse_workspace_with_negated_pattern() {
223 let yaml_content = r#"
224packages:
225 - "packages/*"
226 - "!packages/dont-scan-me"
227"#;
228
229 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
230 let result = parse_workspace_file(&workspace_data);
231
232 let extra_data = result.extra_data.unwrap();
233 let workspaces = extra_data.get("workspaces").unwrap().as_array().unwrap();
234 assert_eq!(workspaces.len(), 2);
235 assert_eq!(workspaces[0], "packages/*");
236 assert_eq!(workspaces[1], "!packages/dont-scan-me");
237 }
238
239 #[test]
240 fn test_parse_workspace_with_depth_pattern() {
241 let yaml_content = r#"
242packages:
243 - "**/components/*"
244"#;
245
246 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
247 let result = parse_workspace_file(&workspace_data);
248
249 let extra_data = result.extra_data.unwrap();
250 let workspaces = extra_data.get("workspaces").unwrap().as_array().unwrap();
251 assert_eq!(workspaces.len(), 1);
252 assert_eq!(workspaces[0], "**/components/*");
253 }
254
255 #[test]
256 fn test_parse_workspace_with_no_packages() {
257 let yaml_content = r#"
258name: my-workspace
259"#;
260
261 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
262 let result = parse_workspace_file(&workspace_data);
263
264 assert_eq!(result.package_type, Some(PackageType::Npm));
265 assert!(result.extra_data.is_some());
266 let extra_data = result.extra_data.unwrap();
267 assert_eq!(
268 extra_data.get("datasource_id").unwrap().as_str().unwrap(),
269 "pnpm_workspace_yaml"
270 );
271 assert!(!extra_data.contains_key("workspaces"));
272 }
273
274 #[test]
275 fn test_parse_workspace_with_empty_packages_array() {
276 let yaml_content = r#"
277packages: []
278"#;
279
280 let workspace_data: Value = yaml_serde::from_str(yaml_content).unwrap();
281 let result = parse_workspace_file(&workspace_data);
282
283 assert_eq!(result.package_type, Some(PackageType::Npm));
284 assert!(
285 result.extra_data.is_none() || !result.extra_data.unwrap().contains_key("workspaces")
286 );
287 }
288
289 #[test]
290 fn test_default_package_data() {
291 let result = default_package_data();
292
293 assert_eq!(result.package_type, Some(PackageType::Npm));
294 assert!(result.name.is_none());
295 assert!(result.version.is_none());
296 assert!(result.extra_data.is_none());
297 }
298}