1use crate::models::{DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage};
24use crate::parser_warn as warn;
25use crate::parsers::utils::{npm_purl, parse_sri};
26use serde_json::Value as JsonValue;
27use std::collections::{HashMap, HashSet};
28use std::fs;
29use std::path::Path;
30use yaml_serde::Value;
31
32use super::PackageParser;
33
34pub struct YarnLockParser;
38
39#[derive(Clone, Debug, PartialEq, Eq)]
40struct ManifestDependencyInfo {
41 scope: &'static str,
42 is_runtime: bool,
43 is_optional: bool,
44}
45
46impl PackageParser for YarnLockParser {
47 const PACKAGE_TYPE: PackageType = PackageType::Npm;
48
49 fn is_match(path: &Path) -> bool {
50 path.file_name()
51 .and_then(|name| name.to_str())
52 .map(|name| name == "yarn.lock")
53 .unwrap_or(false)
54 }
55
56 fn extract_packages(path: &Path) -> Vec<PackageData> {
57 let content = match fs::read_to_string(path) {
58 Ok(content) => content,
59 Err(e) => {
60 warn!("Failed to read yarn.lock at {:?}: {}", path, e);
61 return vec![default_package_data(Some(DatasourceId::YarnLock))];
62 }
63 };
64
65 let is_v2 = detect_yarn_version(&content);
66 let manifest_dependencies = load_manifest_dependency_info(path);
67
68 vec![if is_v2 {
69 parse_yarn_v2(&content, &manifest_dependencies)
70 } else {
71 parse_yarn_v1(&content, &manifest_dependencies)
72 }]
73 }
74}
75
76pub fn detect_yarn_version(content: &str) -> bool {
78 content
79 .lines()
80 .take(10)
81 .any(|line| line.contains("__metadata:"))
82}
83
84fn parse_yarn_v2(
86 content: &str,
87 manifest_dependencies: &HashMap<String, ManifestDependencyInfo>,
88) -> PackageData {
89 let yaml_value: Value = match yaml_serde::from_str(content) {
90 Ok(val) => val,
91 Err(e) => {
92 warn!("Failed to parse yarn.lock v2 YAML: {}", e);
93 return default_package_data(Some(DatasourceId::YarnLockV2));
94 }
95 };
96
97 let yaml_map = match yaml_value.as_mapping() {
98 Some(map) => map,
99 None => return default_package_data(Some(DatasourceId::YarnLockV2)),
100 };
101
102 let mut dependencies = Vec::new();
103 let package_extra_data = extract_yarn_v2_package_extra_data(yaml_map);
104
105 for (spec, details) in yaml_map {
106 if spec.as_str().map(|s| s == "__metadata").unwrap_or(false) {
107 continue;
108 }
109
110 let _spec_str = match spec.as_str() {
111 Some(s) => s,
112 None => continue,
113 };
114
115 let details_map = match details.as_mapping() {
116 Some(map) => map,
117 None => continue,
118 };
119
120 let _version = extract_yaml_string(details_map, "version").unwrap_or_default();
121 let resolution = extract_yaml_string(details_map, "resolution").unwrap_or_default();
122
123 let (namespace_opt, name, resolved_version) = parse_yarn_v2_resolution(&resolution);
124 let namespace = namespace_opt.unwrap_or_default();
125 let full_name = full_package_name(&namespace, &name);
126 let manifest_info = manifest_dependencies.get(&full_name);
127 let purl = create_purl(&namespace, &name, &resolved_version);
128 let checksum = extract_yaml_string(details_map, "checksum");
129
130 let deps_yaml = details_map.get("dependencies");
131 let peer_deps_yaml = details_map.get("peerDependencies");
132 let resolved_extra_data = extract_yarn_v2_resolved_extra_data(details_map, &resolution);
133
134 let nested_deps = parse_yaml_dependencies(deps_yaml);
135 let peer_deps = parse_yaml_dependencies(peer_deps_yaml);
136
137 let all_deps = if peer_deps.is_empty() {
138 nested_deps
139 } else {
140 let mut combined = nested_deps;
141 for mut dep in peer_deps {
142 dep.scope = Some("peerDependencies".to_string());
143 dep.is_optional = Some(true);
144 dep.is_runtime = Some(false);
145 combined.push(dep);
146 }
147 combined
148 };
149
150 let resolved_package = ResolvedPackage {
151 primary_language: Some("JavaScript".to_string()),
152 download_url: None,
153 sha1: None,
154 sha256: None,
155 sha512: checksum,
156 md5: None,
157 is_virtual: true,
158 extra_data: resolved_extra_data,
159 dependencies: all_deps,
160 repository_homepage_url: None,
161 repository_download_url: None,
162 api_data_url: None,
163 datasource_id: Some(DatasourceId::YarnLockV2),
164 purl: None,
165 ..ResolvedPackage::new(
166 YarnLockParser::PACKAGE_TYPE,
167 namespace.clone(),
168 name.clone(),
169 resolved_version.clone(),
170 )
171 };
172
173 let (scope, is_runtime, is_optional, is_direct) = manifest_info
174 .map(|info| {
175 (
176 info.scope.to_string(),
177 info.is_runtime,
178 info.is_optional,
179 true,
180 )
181 })
182 .unwrap_or_else(|| {
183 (
184 "dependencies".to_string(),
185 true,
186 false,
187 resolution.contains("workspace:"),
188 )
189 });
190
191 let dependency = Dependency {
192 purl,
193 extracted_requirement: Some(resolved_version.clone()),
194 scope: Some(scope),
195 is_runtime: Some(is_runtime),
196 is_optional: Some(is_optional),
197 is_pinned: Some(true),
198 is_direct: Some(is_direct),
199 resolved_package: Some(Box::new(resolved_package)),
200 extra_data: Some(HashMap::from([(
201 "resolution".to_string(),
202 JsonValue::String(resolution),
203 )])),
204 };
205
206 dependencies.push(dependency);
207 }
208
209 PackageData {
210 package_type: Some(YarnLockParser::PACKAGE_TYPE),
211 namespace: None,
212 name: None,
213 version: None,
214 qualifiers: None,
215 subpath: None,
216 primary_language: None,
217 description: None,
218 release_date: None,
219 parties: Vec::new(),
220 keywords: Vec::new(),
221 homepage_url: None,
222 download_url: None,
223 size: None,
224 sha1: None,
225 md5: None,
226 sha256: None,
227 sha512: None,
228 bug_tracking_url: None,
229 code_view_url: None,
230 vcs_url: None,
231 copyright: None,
232 holder: None,
233 declared_license_expression: None,
234 declared_license_expression_spdx: None,
235 license_detections: Vec::new(),
236 other_license_expression: None,
237 other_license_expression_spdx: None,
238 other_license_detections: Vec::new(),
239 extracted_license_statement: None,
240 notice_text: None,
241 source_packages: Vec::new(),
242 file_references: Vec::new(),
243 is_private: false,
244 is_virtual: false,
245 extra_data: package_extra_data,
246 dependencies,
247 repository_homepage_url: None,
248 repository_download_url: None,
249 api_data_url: None,
250 datasource_id: Some(DatasourceId::YarnLockV2),
251 purl: None,
252 }
253}
254
255fn parse_yarn_v1(
257 content: &str,
258 manifest_dependencies: &HashMap<String, ManifestDependencyInfo>,
259) -> PackageData {
260 let mut dependencies = Vec::new();
261 let mut seen_purls = HashSet::new();
262
263 for block in content.split("\n\n") {
264 if is_empty_or_comment_block(block) {
265 continue;
266 }
267
268 if let Some(dep) = parse_yarn_v1_block(block, manifest_dependencies) {
269 if let Some(ref purl) = dep.purl {
270 if seen_purls.insert(purl.clone()) {
271 dependencies.push(dep);
272 }
273 } else {
274 dependencies.push(dep);
275 }
276 }
277 }
278
279 PackageData {
280 package_type: Some(YarnLockParser::PACKAGE_TYPE),
281 namespace: None,
282 name: None,
283 version: None,
284 qualifiers: None,
285 subpath: None,
286 primary_language: None,
287 description: None,
288 release_date: None,
289 parties: Vec::new(),
290 keywords: Vec::new(),
291 homepage_url: None,
292 download_url: None,
293 size: None,
294 sha1: None,
295 md5: None,
296 sha256: None,
297 sha512: None,
298 bug_tracking_url: None,
299 code_view_url: None,
300 vcs_url: None,
301 copyright: None,
302 holder: None,
303 declared_license_expression: None,
304 declared_license_expression_spdx: None,
305 license_detections: Vec::new(),
306 other_license_expression: None,
307 other_license_expression_spdx: None,
308 other_license_detections: Vec::new(),
309 extracted_license_statement: None,
310 notice_text: None,
311 source_packages: Vec::new(),
312 file_references: Vec::new(),
313 is_private: false,
314 is_virtual: false,
315 extra_data: None,
316 dependencies,
317 repository_homepage_url: None,
318 repository_download_url: None,
319 api_data_url: None,
320 datasource_id: Some(DatasourceId::YarnLockV1),
321 purl: None,
322 }
323}
324
325fn is_empty_or_comment_block(block: &str) -> bool {
326 block
327 .lines()
328 .all(|line| line.trim().is_empty() || line.trim().starts_with('#'))
329}
330
331fn parse_integrity_field(integrity: &str) -> Option<String> {
333 parse_sri(integrity).and_then(|(algo, hex_digest)| {
334 if algo == "sha512" {
335 Some(hex_digest)
336 } else {
337 None
338 }
339 })
340}
341
342pub fn extract_namespace_and_name(package_name: &str) -> (String, String) {
344 if package_name.starts_with('@') {
345 let parts: Vec<&str> = package_name.splitn(2, '/').collect();
346 if parts.len() == 2 {
347 (parts[0].to_string(), parts[1].to_string())
348 } else {
349 (String::new(), package_name.to_string())
350 }
351 } else {
352 (String::new(), package_name.to_string())
353 }
354}
355
356fn create_purl(namespace: &str, name: &str, version: &str) -> Option<String> {
357 let full_name = if namespace.is_empty() {
358 name.to_string()
359 } else {
360 format!("{}/{}", namespace, name)
361 };
362 let version_opt = if version.is_empty() {
363 None
364 } else {
365 Some(version)
366 };
367 npm_purl(&full_name, version_opt)
368}
369
370fn default_package_data(datasource_id: Option<DatasourceId>) -> PackageData {
371 PackageData {
372 package_type: Some(YarnLockParser::PACKAGE_TYPE),
373 datasource_id,
374 ..Default::default()
375 }
376}
377
378fn parse_yarn_v1_block(
380 block: &str,
381 manifest_dependencies: &HashMap<String, ManifestDependencyInfo>,
382) -> Option<Dependency> {
383 let lines: Vec<&str> = block.lines().collect();
384 if lines.is_empty() {
385 return None;
386 }
387
388 let requirement_line = lines[0]
389 .trim()
390 .strip_suffix(':')
391 .unwrap_or_else(|| lines[0].trim())
392 .trim_matches('"');
393 if requirement_line.is_empty() || requirement_line.starts_with('#') {
394 return None;
395 }
396
397 let (namespace, name, constraint) = parse_yarn_v1_requirement(requirement_line);
398
399 if name.is_empty() {
400 return None;
401 }
402
403 let mut version = String::new();
404 let mut resolved_url = String::new();
405 let mut integrity = String::new();
406 let mut nested_deps = Vec::new();
407
408 for line in &lines[1..] {
409 let trimmed = line.trim();
410 if trimmed.is_empty() {
411 continue;
412 }
413
414 if trimmed.starts_with("version") {
415 version = extract_quoted_value(trimmed).unwrap_or_default();
416 } else if trimmed.starts_with("resolved") {
417 resolved_url = extract_quoted_value(trimmed).unwrap_or_default();
418 } else if trimmed.starts_with("integrity") {
419 integrity = trimmed
420 .strip_prefix("integrity")
421 .map(|s| s.trim().to_string())
422 .unwrap_or_default();
423 } else if trimmed.starts_with("dependencies") {
424 continue;
426 } else if trimmed.starts_with(" ") && !trimmed.starts_with(" ") {
427 let dep_line = trimmed.trim();
429 if let Some(dep) = parse_yarn_v1_dependency_line(dep_line, &namespace, &name, &version)
430 {
431 nested_deps.push(dep);
432 }
433 }
434 }
435
436 let sha512 = if integrity.is_empty() {
437 None
438 } else {
439 parse_integrity_field(&integrity)
440 };
441
442 let full_name = full_package_name(&namespace, &name);
443 let manifest_info = manifest_dependencies.get(&full_name);
444 let purl = create_purl(&namespace, &name, &version);
445 let (scope, is_runtime, is_optional, is_direct) = manifest_info
446 .map(|info| {
447 (
448 info.scope.to_string(),
449 info.is_runtime,
450 info.is_optional,
451 true,
452 )
453 })
454 .unwrap_or_else(|| ("dependencies".to_string(), true, false, false));
455
456 let resolved_package = ResolvedPackage {
457 primary_language: Some("JavaScript".to_string()),
458 download_url: if resolved_url.is_empty() {
459 None
460 } else {
461 Some(resolved_url)
462 },
463 sha1: None,
464 sha256: None,
465 sha512,
466 md5: None,
467 is_virtual: true,
468 extra_data: None,
469 dependencies: nested_deps,
470 repository_homepage_url: None,
471 repository_download_url: None,
472 api_data_url: None,
473 datasource_id: Some(DatasourceId::YarnLockV1),
474 purl: None,
475 ..ResolvedPackage::new(
476 YarnLockParser::PACKAGE_TYPE,
477 namespace.clone(),
478 name.clone(),
479 version.clone(),
480 )
481 };
482
483 Some(Dependency {
484 purl,
485 extracted_requirement: Some(constraint),
486 scope: Some(scope),
487 is_runtime: Some(is_runtime),
488 is_optional: Some(is_optional),
489 is_pinned: Some(true),
490 is_direct: Some(is_direct),
491 resolved_package: Some(Box::new(resolved_package)),
492 extra_data: None,
493 })
494}
495
496fn full_package_name(namespace: &str, name: &str) -> String {
497 if namespace.is_empty() {
498 name.to_string()
499 } else {
500 format!("{namespace}/{name}")
501 }
502}
503
504fn load_manifest_dependency_info(path: &Path) -> HashMap<String, ManifestDependencyInfo> {
505 let Some(parent) = path.parent() else {
506 return HashMap::new();
507 };
508
509 let manifest_path = parent.join("package.json");
510 let Ok(content) = fs::read_to_string(manifest_path) else {
511 return HashMap::new();
512 };
513
514 let Ok(json) = serde_json::from_str::<JsonValue>(&content) else {
515 return HashMap::new();
516 };
517
518 let peer_optional = json
519 .get("peerDependenciesMeta")
520 .and_then(|value| value.as_object())
521 .map(|meta| {
522 meta.iter()
523 .filter_map(|(name, value)| {
524 value
525 .as_object()
526 .and_then(|entry| entry.get("optional"))
527 .and_then(|value| value.as_bool())
528 .map(|optional| (name.clone(), optional))
529 })
530 .collect::<HashMap<_, _>>()
531 })
532 .unwrap_or_default();
533
534 let mut dependencies = HashMap::new();
535 insert_manifest_dependency_info(
536 &mut dependencies,
537 &json,
538 "dependencies",
539 ManifestDependencyInfo {
540 scope: "dependencies",
541 is_runtime: true,
542 is_optional: false,
543 },
544 );
545 insert_manifest_dependency_info(
546 &mut dependencies,
547 &json,
548 "devDependencies",
549 ManifestDependencyInfo {
550 scope: "devDependencies",
551 is_runtime: false,
552 is_optional: true,
553 },
554 );
555 insert_manifest_dependency_info(
556 &mut dependencies,
557 &json,
558 "optionalDependencies",
559 ManifestDependencyInfo {
560 scope: "optionalDependencies",
561 is_runtime: true,
562 is_optional: true,
563 },
564 );
565
566 if let Some(peer_dependencies) = json
567 .get("peerDependencies")
568 .and_then(|value| value.as_object())
569 {
570 for name in peer_dependencies.keys() {
571 dependencies.insert(
572 name.clone(),
573 ManifestDependencyInfo {
574 scope: "peerDependencies",
575 is_runtime: false,
576 is_optional: peer_optional.get(name).copied().unwrap_or(false),
577 },
578 );
579 }
580 }
581
582 dependencies
583}
584
585fn insert_manifest_dependency_info(
586 dependencies: &mut HashMap<String, ManifestDependencyInfo>,
587 json: &JsonValue,
588 field: &str,
589 info: ManifestDependencyInfo,
590) {
591 if let Some(entries) = json.get(field).and_then(|value| value.as_object()) {
592 for name in entries.keys() {
593 dependencies.insert(name.clone(), info.clone());
594 }
595 }
596}
597
598pub fn parse_yarn_v1_requirement(line: &str) -> (String, String, String) {
600 if line.contains(", ") {
602 let first_part = line.split(", ").next().unwrap_or(line);
604 return parse_single_yarn_v1_requirement(first_part);
605 }
606 parse_single_yarn_v1_requirement(line)
607}
608
609fn parse_single_yarn_v1_requirement(line: &str) -> (String, String, String) {
611 if let Some(at_pos) = line.rfind('@') {
612 let name_part = &line[..at_pos];
613 let constraint = &line[at_pos + 1..];
614 let (namespace, name) = extract_namespace_and_name(name_part);
615
616 if !name.is_empty() {
617 return (namespace, name, constraint.to_string());
618 }
619 }
620
621 (String::new(), String::new(), String::new())
622}
623
624fn parse_yarn_v1_dependency_line(
626 line: &str,
627 _parent_namespace: &str,
628 _parent_name: &str,
629 parent_version: &str,
630) -> Option<Dependency> {
631 let trimmed = line.trim_matches('"');
632 if !trimmed.contains('@') {
633 return None;
634 }
635
636 let (namespace, name, constraint) = parse_yarn_v1_requirement(trimmed);
637
638 let purl = create_purl(&namespace, &name, parent_version);
639
640 Some(Dependency {
641 purl,
642 extracted_requirement: Some(constraint),
643 scope: Some("dependencies".to_string()),
644 is_runtime: Some(true),
645 is_optional: Some(false),
646 is_pinned: Some(false),
647 is_direct: Some(false),
648 resolved_package: None,
649 extra_data: None,
650 })
651}
652
653fn extract_quoted_value(line: &str) -> Option<String> {
655 line.find('"').and_then(|start| {
656 let rest = &line[start + 1..];
657 rest.find('"').map(|end| rest[..end].to_string())
658 })
659}
660
661pub fn parse_yarn_v2_resolution(resolution: &str) -> (Option<String>, String, String) {
663 if resolution.contains("@npm:") {
664 let parts: Vec<&str> = resolution.split("@npm:").collect();
665 if parts.len() == 2 {
666 let package_name = parts[0];
667 let version = parts[1];
668 let (namespace, name) = extract_namespace_and_name(package_name);
669 let namespace_opt = if namespace.is_empty() {
670 None
671 } else {
672 Some(namespace)
673 };
674 return (namespace_opt, name, version.to_string());
675 }
676 }
677
678 if let Some((ident, reference)) = split_yarn_locator(resolution) {
679 let (namespace, name) = extract_namespace_and_name(ident);
680 let namespace_opt = if namespace.is_empty() {
681 None
682 } else {
683 Some(namespace)
684 };
685 return (namespace_opt, name, reference.to_string());
686 }
687
688 let (namespace, name) = extract_namespace_and_name(resolution);
689 let namespace_opt = if namespace.is_empty() {
690 None
691 } else {
692 Some(namespace)
693 };
694 (namespace_opt, name, "*".to_string())
695}
696
697fn split_yarn_locator(resolution: &str) -> Option<(&str, &str)> {
698 if resolution.is_empty() {
699 return None;
700 }
701
702 let separator_index = if resolution.starts_with('@') {
703 let slash_index = resolution.find('/')?;
704 let rest = &resolution[slash_index + 1..];
705 let at_index = rest.find('@')?;
706 slash_index + 1 + at_index
707 } else {
708 resolution.find('@')?
709 };
710
711 let ident = &resolution[..separator_index];
712 let reference = &resolution[separator_index + 1..];
713
714 if ident.is_empty() || reference.is_empty() {
715 None
716 } else {
717 Some((ident, reference))
718 }
719}
720
721fn extract_yarn_v2_package_extra_data(
722 yaml_map: &yaml_serde::Mapping,
723) -> Option<HashMap<String, JsonValue>> {
724 let metadata = yaml_map.get("__metadata")?.as_mapping()?;
725 let mut extra_data = HashMap::new();
726
727 for field in ["version", "cacheKey"] {
728 if let Some(value) = metadata.get(field).and_then(yaml_value_to_json) {
729 extra_data.insert(field.to_string(), value);
730 }
731 }
732
733 (!extra_data.is_empty()).then_some(extra_data)
734}
735
736fn extract_yarn_v2_resolved_extra_data(
737 details_map: &yaml_serde::Mapping,
738 resolution: &str,
739) -> Option<HashMap<String, JsonValue>> {
740 let mut extra_data = HashMap::new();
741 extra_data.insert(
742 "resolution".to_string(),
743 JsonValue::String(resolution.to_string()),
744 );
745
746 for field in ["languageName", "linkType", "bin", "dependenciesMeta"] {
747 if let Some(value) = details_map.get(field).and_then(yaml_value_to_json) {
748 extra_data.insert(field.to_string(), value);
749 }
750 }
751
752 Some(extra_data)
753}
754
755fn yaml_value_to_json(value: &Value) -> Option<JsonValue> {
756 serde_json::to_value(value).ok()
757}
758
759fn extract_yaml_string(map: &yaml_serde::Mapping, key: &str) -> Option<String> {
761 map.get(key).and_then(|v| v.as_str()).map(|s| s.to_string())
762}
763
764fn parse_yaml_dependencies(yaml_value: Option<&Value>) -> Vec<Dependency> {
766 let mut dependencies = Vec::new();
767
768 if let Some(deps_value) = yaml_value
769 && let Some(mapping) = deps_value.as_mapping()
770 {
771 for (key, value) in mapping {
772 let name = match key.as_str() {
773 Some(s) => s.to_string(),
774 None => continue,
775 };
776
777 let constraint = match value.as_str() {
778 Some(s) => s.to_string(),
779 None => "*".to_string(),
780 };
781
782 let (namespace, dep_name) = extract_namespace_and_name(&name);
783 let purl = create_purl(&namespace, &dep_name, &constraint);
784
785 dependencies.push(Dependency {
786 purl,
787 extracted_requirement: Some(constraint),
788 scope: Some("dependencies".to_string()),
789 is_runtime: Some(true),
790 is_optional: Some(false),
791 is_pinned: Some(false),
792 is_direct: Some(false),
793 resolved_package: None,
794 extra_data: None,
795 });
796 }
797 }
798 dependencies
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804 use std::path::PathBuf;
805
806 #[test]
807 fn test_is_match_yarn_lock() {
808 let valid_path = PathBuf::from("/some/path/yarn.lock");
809 assert!(YarnLockParser::is_match(&valid_path));
810 }
811
812 #[test]
813 fn test_is_not_match_package_json() {
814 let invalid_path = PathBuf::from("/some/path/package.json");
815 assert!(!YarnLockParser::is_match(&invalid_path));
816 }
817
818 #[test]
819 fn test_detect_yarn_v2() {
820 let content = r#"# This file is generated by running "yarn install"
821__metadata:
822 version: 6
823"#;
824 assert!(detect_yarn_version(content));
825 }
826
827 #[test]
828 fn test_detect_yarn_v1() {
829 let content = r#"# THIS IS AN AUTOGENERATED FILE
830# yarn lockfile v1
831
832abbrev@1:
833 version "1.0.9"
834"#;
835 assert!(!detect_yarn_version(content));
836 }
837
838 #[test]
839 fn test_parse_yarn_v2_uses_yarn_lock_v2_datasource_ids() {
840 let content = r#"# This file is generated by running \"yarn install\"
841__metadata:
842 version: 6
843
844lodash@npm:^4.17.21:
845 version: 4.17.21
846 resolution: "lodash@npm:4.17.21"
847"#;
848
849 let package_data = parse_yarn_v2(content, &HashMap::new());
850
851 assert_eq!(package_data.datasource_id, Some(DatasourceId::YarnLockV2));
852 assert_eq!(
853 package_data.dependencies[0]
854 .resolved_package
855 .as_ref()
856 .and_then(|pkg| pkg.datasource_id),
857 Some(DatasourceId::YarnLockV2)
858 );
859 }
860
861 #[test]
862 fn test_parse_yarn_v1_uses_yarn_lock_v1_datasource_ids() {
863 let content = r#"# THIS IS AN AUTOGENERATED FILE
864# yarn lockfile v1
865
866left-pad@^1.3.0:
867 version \"1.3.0\"
868"#;
869
870 let package_data = parse_yarn_v1(content, &HashMap::new());
871
872 assert_eq!(package_data.datasource_id, Some(DatasourceId::YarnLockV1));
873 assert_eq!(
874 package_data.dependencies[0]
875 .resolved_package
876 .as_ref()
877 .and_then(|pkg| pkg.datasource_id),
878 Some(DatasourceId::YarnLockV1)
879 );
880 }
881
882 #[test]
883 fn test_extract_namespace_and_name_scoped() {
884 let (namespace, name) = extract_namespace_and_name("@types/node");
885 assert_eq!(namespace, "@types");
886 assert_eq!(name, "node");
887 }
888
889 #[test]
890 fn test_extract_namespace_and_name_regular() {
891 let (namespace, name) = extract_namespace_and_name("express");
892 assert_eq!(namespace, "");
893 assert_eq!(name, "express");
894 }
895
896 #[test]
897 fn test_parse_yarn_v1_requirement() {
898 let (namespace, name, constraint) = parse_yarn_v1_requirement("express@^4.0.0");
899 assert_eq!(namespace, "");
900 assert_eq!(name, "express");
901 assert_eq!(constraint, "^4.0.0");
902 }
903
904 #[test]
905 fn test_parse_yarn_v1_requirement_scoped() {
906 let (namespace, name, constraint) = parse_yarn_v1_requirement("@types/node@^18.0.0");
907 assert_eq!(namespace, "@types");
908 assert_eq!(name, "node");
909 assert_eq!(constraint, "^18.0.0");
910 }
911
912 #[test]
913 fn test_parse_yarn_v2_resolution() {
914 let (namespace, name, version) = parse_yarn_v2_resolution("@actions/core@npm:1.2.6");
915 assert_eq!(namespace, Some("@actions".to_string()));
916 assert_eq!(name, "core");
917 assert_eq!(version, "1.2.6");
918 }
919}
920
921crate::register_parser!(
922 "yarn.lock lockfile (v1 and v2+)",
923 &["**/yarn.lock"],
924 "npm",
925 "JavaScript",
926 Some("https://classic.yarnpkg.com/lang/en/docs/yarn-lock/"),
927);