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