1use crate::models::{
26 DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage, Sha1Digest, Sha512Digest,
27};
28use crate::parser_warn as warn;
29use crate::parsers::utils::{
30 MAX_ITERATION_COUNT, MAX_RECURSION_DEPTH, RecursionGuard, npm_purl, parse_sri,
31 read_file_to_string, truncate_field,
32};
33use serde_json::Value;
34use std::collections::HashMap;
35use std::path::Path;
36
37use super::PackageParser;
38
39const FIELD_LOCKFILE_VERSION: &str = "lockfileVersion";
41const FIELD_NAME: &str = "name";
42const FIELD_VERSION: &str = "version";
43const FIELD_DEPENDENCIES: &str = "dependencies";
44const FIELD_PACKAGES: &str = "packages";
45const FIELD_RESOLVED: &str = "resolved";
46const FIELD_INTEGRITY: &str = "integrity";
47const FIELD_DEV: &str = "dev";
48const FIELD_OPTIONAL: &str = "optional";
49const FIELD_DEV_OPTIONAL: &str = "devOptional";
50const FIELD_LINK: &str = "link";
51
52pub struct NpmLockParser;
57
58impl PackageParser for NpmLockParser {
59 const PACKAGE_TYPE: PackageType = PackageType::Npm;
60
61 fn is_match(path: &Path) -> bool {
62 path.file_name()
63 .and_then(|name| name.to_str())
64 .map(|name| {
65 name == "package-lock.json"
66 || name == ".package-lock.json"
67 || name == "npm-shrinkwrap.json"
68 || name == ".npm-shrinkwrap.json"
69 })
70 .unwrap_or(false)
71 }
72
73 fn extract_packages(path: &Path) -> Vec<PackageData> {
74 let content = match read_file_to_string(path, None) {
75 Ok(content) => content,
76 Err(e) => {
77 warn!("Failed to read package-lock.json at {:?}: {}", path, e);
78 return vec![default_package_data()];
79 }
80 };
81
82 let json: Value = match serde_json::from_str(&content) {
83 Ok(json) => json,
84 Err(e) => {
85 warn!("Failed to parse package-lock.json at {:?}: {}", path, e);
86 return vec![default_package_data()];
87 }
88 };
89
90 let lockfile_version = json
91 .get(FIELD_LOCKFILE_VERSION)
92 .and_then(|v| v.as_i64())
93 .unwrap_or(1);
94
95 let root_name = truncate_field(
96 json.get(FIELD_NAME)
97 .and_then(|v| v.as_str())
98 .unwrap_or("")
99 .to_string(),
100 );
101
102 let root_version = truncate_field(
103 json.get(FIELD_VERSION)
104 .and_then(|v| v.as_str())
105 .unwrap_or("")
106 .to_string(),
107 );
108
109 vec![if lockfile_version == 1 {
110 parse_lockfile_v1(&json, root_name, root_version, lockfile_version)
111 } else {
112 parse_lockfile_v2_plus(&json, root_name, root_version, lockfile_version)
113 }]
114 }
115}
116
117fn default_package_data() -> PackageData {
119 PackageData {
120 package_type: Some(NpmLockParser::PACKAGE_TYPE),
121 datasource_id: Some(DatasourceId::NpmPackageLockJson),
122 ..Default::default()
123 }
124}
125
126fn parse_lockfile_v2_plus(
128 json: &Value,
129 root_name: String,
130 root_version: String,
131 lockfile_version: i64,
132) -> PackageData {
133 let packages = match json.get(FIELD_PACKAGES).and_then(|v| v.as_object()) {
134 Some(packages) => packages,
135 None => {
136 warn!("No 'packages' field found in lockfile v2+");
137 return default_package_data();
138 }
139 };
140
141 let (root_name, root_version) = extract_root_package_identity(json, root_name, root_version);
142 let (namespace, name, version, purl) =
143 normalize_root_package_metadata(&root_name, &root_version);
144 let linked_workspace_names = collect_linked_workspace_names(packages);
145
146 let mut root_deps = std::collections::HashSet::new();
148
149 if let Some(root_deps_obj) = json.get(FIELD_DEPENDENCIES).and_then(|v| v.as_object()) {
151 for key in root_deps_obj.keys().take(MAX_ITERATION_COUNT) {
152 root_deps.insert(key.clone());
153 }
154 }
155 if let Some(root_dev_deps_obj) = json.get("devDependencies").and_then(|v| v.as_object()) {
156 for key in root_dev_deps_obj.keys().take(MAX_ITERATION_COUNT) {
157 root_deps.insert(key.clone());
158 }
159 }
160 if let Some(root_package) = packages.get("").and_then(|value| value.as_object()) {
161 collect_root_dependency_names(root_package.get(FIELD_DEPENDENCIES), &mut root_deps);
162 collect_root_dependency_names(root_package.get("devDependencies"), &mut root_deps);
163 collect_root_dependency_names(root_package.get("optionalDependencies"), &mut root_deps);
164 }
165
166 let mut dependencies = Vec::new();
167
168 for (key, value) in packages.iter().take(MAX_ITERATION_COUNT) {
169 if key.is_empty() {
171 continue;
172 }
173
174 let install_name = extract_package_name_from_path(key);
176 if install_name.is_empty() {
177 continue;
178 }
179
180 let package_name = value
181 .get(FIELD_NAME)
182 .and_then(|v| v.as_str())
183 .map(str::trim)
184 .filter(|name| !name.is_empty())
185 .map(str::to_string)
186 .or_else(|| linked_workspace_names.get(key).cloned())
187 .unwrap_or_else(|| install_name.clone());
188
189 let version = value
190 .get(FIELD_VERSION)
191 .and_then(|v| v.as_str())
192 .map(|v| truncate_field(v.to_string()));
193
194 let is_dev = value
195 .get(FIELD_DEV)
196 .and_then(|v| v.as_bool())
197 .unwrap_or(false);
198 let is_optional = value
199 .get(FIELD_OPTIONAL)
200 .and_then(|v| v.as_bool())
201 .unwrap_or(false);
202 let is_dev_optional = value
203 .get(FIELD_DEV_OPTIONAL)
204 .and_then(|v| v.as_bool())
205 .unwrap_or(false);
206
207 let resolved = value
208 .get(FIELD_RESOLVED)
209 .and_then(|v| v.as_str())
210 .map(|v| truncate_field(v.to_string()));
211 let integrity = value.get(FIELD_INTEGRITY).and_then(|v| v.as_str());
212 let from = value.get("from").and_then(|v| v.as_str());
213 let in_bundle = value
214 .get("inBundle")
215 .and_then(|v| v.as_bool())
216 .unwrap_or(false);
217 let is_link = value
218 .get(FIELD_LINK)
219 .and_then(|v| v.as_bool())
220 .unwrap_or(false);
221 let is_direct = root_deps.contains(&install_name) && is_direct_dependency_path(key);
222
223 let dependency = match version {
224 Some(version) => build_npm_dependency(
225 &package_name,
226 version,
227 is_dev,
228 is_dev_optional,
229 is_optional,
230 resolved,
231 integrity,
232 is_direct,
233 from,
234 in_bundle,
235 Vec::new(),
236 ),
237 None if is_link => build_link_dependency(
238 &package_name,
239 is_dev,
240 is_dev_optional,
241 is_optional,
242 resolved,
243 is_direct,
244 ),
245 None => continue,
246 };
247
248 dependencies.push(dependency);
249 }
250
251 let extra_data = Some(HashMap::from([(
252 "lockfileVersion".to_string(),
253 Value::from(lockfile_version),
254 )]));
255
256 PackageData {
257 package_type: Some(NpmLockParser::PACKAGE_TYPE),
258 namespace: namespace.clone(),
259 name,
260 version,
261 qualifiers: None,
262 subpath: None,
263 primary_language: None,
264 description: None,
265 release_date: None,
266 parties: Vec::new(),
267 keywords: Vec::new(),
268 homepage_url: None,
269 download_url: None,
270 size: None,
271 sha1: None,
272 md5: None,
273 sha256: None,
274 sha512: None,
275 bug_tracking_url: None,
276 code_view_url: None,
277 vcs_url: None,
278 copyright: None,
279 holder: None,
280 declared_license_expression: None,
281 declared_license_expression_spdx: None,
282 license_detections: Vec::new(),
283 other_license_expression: None,
284 other_license_expression_spdx: None,
285 other_license_detections: Vec::new(),
286 extracted_license_statement: None,
287 notice_text: None,
288 source_packages: Vec::new(),
289 file_references: Vec::new(),
290 is_private: false,
291 is_virtual: false,
292 extra_data,
293 dependencies,
294 repository_homepage_url: None,
295 repository_download_url: None,
296 api_data_url: None,
297 datasource_id: Some(DatasourceId::NpmPackageLockJson),
298 purl,
299 }
300}
301
302fn collect_linked_workspace_names(
303 packages: &serde_json::Map<String, Value>,
304) -> HashMap<String, String> {
305 let mut linked_names = HashMap::new();
306
307 for (key, value) in packages.iter().take(MAX_ITERATION_COUNT) {
308 let is_link = value
309 .get(FIELD_LINK)
310 .and_then(|v| v.as_bool())
311 .unwrap_or(false);
312 if !is_link {
313 continue;
314 }
315
316 let Some(resolved) = value.get(FIELD_RESOLVED).and_then(|v| v.as_str()) else {
317 continue;
318 };
319
320 let install_name = extract_package_name_from_path(key);
321 if install_name.is_empty() || install_name == key.as_str() {
322 continue;
323 }
324
325 linked_names.insert(resolved.to_string(), install_name);
326 }
327
328 linked_names
329}
330
331fn parse_lockfile_v1(
333 json: &Value,
334 root_name: String,
335 root_version: String,
336 _lockfile_version: i64,
337) -> PackageData {
338 let dependencies_obj = match json.get(FIELD_DEPENDENCIES).and_then(|v| v.as_object()) {
339 Some(deps) => deps,
340 None => {
341 warn!("No 'dependencies' field found in lockfile v1");
342 return default_package_data();
343 }
344 };
345
346 let (namespace, name, version, purl) =
347 normalize_root_package_metadata(&root_name, &root_version);
348
349 let dependencies = parse_dependencies_v1(dependencies_obj);
350
351 PackageData {
352 package_type: Some(NpmLockParser::PACKAGE_TYPE),
353 namespace: namespace.clone(),
354 name,
355 version,
356 qualifiers: None,
357 subpath: None,
358 primary_language: None,
359 description: None,
360 release_date: None,
361 parties: Vec::new(),
362 keywords: Vec::new(),
363 homepage_url: None,
364 download_url: None,
365 size: None,
366 sha1: None,
367 md5: None,
368 sha256: None,
369 sha512: None,
370 bug_tracking_url: None,
371 code_view_url: None,
372 vcs_url: None,
373 copyright: None,
374 holder: None,
375 declared_license_expression: None,
376 declared_license_expression_spdx: None,
377 license_detections: Vec::new(),
378 other_license_expression: None,
379 other_license_expression_spdx: None,
380 other_license_detections: Vec::new(),
381 extracted_license_statement: None,
382 notice_text: None,
383 source_packages: Vec::new(),
384 file_references: Vec::new(),
385 is_private: false,
386 is_virtual: false,
387 extra_data: None,
388 dependencies,
389 repository_homepage_url: None,
390 repository_download_url: None,
391 api_data_url: None,
392 datasource_id: Some(DatasourceId::NpmPackageLockJson),
393 purl,
394 }
395}
396
397fn parse_dependencies_v1(dependencies_obj: &serde_json::Map<String, Value>) -> Vec<Dependency> {
402 let mut guard = RecursionGuard::<()>::depth_only();
403 parse_dependencies_v1_with_depth(dependencies_obj, &mut guard)
404}
405
406fn parse_dependencies_v1_with_depth(
408 dependencies_obj: &serde_json::Map<String, Value>,
409 guard: &mut RecursionGuard<()>,
410) -> Vec<Dependency> {
411 if guard.descend() {
412 warn!(
413 "Max recursion depth {} exceeded in v1 dependency parsing",
414 MAX_RECURSION_DEPTH
415 );
416 return Vec::new();
417 }
418
419 let mut dependencies = Vec::new();
420
421 for (package_name, dep_data) in dependencies_obj.iter().take(MAX_ITERATION_COUNT) {
422 let version = match dep_data.get(FIELD_VERSION).and_then(|v| v.as_str()) {
423 Some(v) => truncate_field(v.to_string()),
424 None => continue,
425 };
426
427 let is_dev = dep_data
428 .get(FIELD_DEV)
429 .and_then(|v| v.as_bool())
430 .unwrap_or(false);
431 let is_optional = dep_data
432 .get(FIELD_OPTIONAL)
433 .and_then(|v| v.as_bool())
434 .unwrap_or(false);
435
436 let resolved = dep_data
437 .get(FIELD_RESOLVED)
438 .and_then(|v| v.as_str())
439 .map(|v| truncate_field(v.to_string()));
440 let integrity = dep_data.get(FIELD_INTEGRITY).and_then(|v| v.as_str());
441 let from = dep_data.get("from").and_then(|v| v.as_str());
442 let in_bundle = dep_data
443 .get("inBundle")
444 .and_then(|v| v.as_bool())
445 .unwrap_or(false);
446
447 let nested_deps = dep_data
448 .get(FIELD_DEPENDENCIES)
449 .and_then(|v| v.as_object())
450 .map(|nested| parse_dependencies_v1_with_depth(nested, guard))
451 .unwrap_or_default();
452
453 let is_direct = guard.depth() == 1;
454
455 let dependency = build_npm_dependency(
456 package_name,
457 version,
458 is_dev,
459 false,
460 is_optional,
461 resolved,
462 integrity,
463 is_direct,
464 from,
465 in_bundle,
466 nested_deps,
467 );
468
469 dependencies.push(dependency);
470 }
471
472 guard.ascend();
473 dependencies
474}
475
476fn extract_namespace_and_name(package_name: &str) -> (String, String) {
479 if package_name.starts_with('@') {
480 let parts: Vec<&str> = package_name.splitn(2, '/').collect();
482 if parts.len() == 2 {
483 (parts[0].to_string(), parts[1].to_string())
484 } else {
485 (String::new(), package_name.to_string())
487 }
488 } else {
489 (String::new(), package_name.to_string())
491 }
492}
493
494fn extract_package_name_from_path(path: &str) -> String {
496 if let Some(pos) = path.rfind("node_modules/") {
498 let after_node_modules = &path[pos + "node_modules/".len()..];
499
500 if after_node_modules.starts_with('@') {
502 if let Some(slash_pos) = after_node_modules.find('/') {
504 let scope_and_package = &after_node_modules[..=slash_pos];
505 let remaining = &after_node_modules[slash_pos + 1..];
507 if let Some(next_slash) = remaining.find('/') {
508 return format!("{}{}", scope_and_package, &remaining[..next_slash]);
510 } else {
511 return after_node_modules.to_string();
513 }
514 }
515 } else {
516 if let Some(slash_pos) = after_node_modules.find('/') {
518 return after_node_modules[..slash_pos].to_string();
519 } else {
520 return after_node_modules.to_string();
521 }
522 }
523 }
524
525 path.to_string()
526}
527
528fn create_purl(namespace: &str, name: &str, version: Option<&str>) -> Option<String> {
529 let full_name = if namespace.is_empty() {
530 name.to_string()
531 } else {
532 format!("{}/{}", namespace, name)
533 };
534 npm_purl(&full_name, version.filter(|value| !value.is_empty()))
535}
536
537fn normalize_root_package_metadata(
538 root_name: &str,
539 root_version: &str,
540) -> (
541 Option<String>,
542 Option<String>,
543 Option<String>,
544 Option<String>,
545) {
546 let (namespace, name) = extract_namespace_and_name(root_name);
547 let normalized_name = non_empty_string(&name);
548 let normalized_namespace = normalized_name.as_ref().map(|_| namespace);
549 let normalized_version = normalized_name
550 .as_ref()
551 .and_then(|_| non_empty_string(root_version));
552 let purl = normalized_name.as_deref().and_then(|name| {
553 create_purl(
554 normalized_namespace.as_deref().unwrap_or(""),
555 name,
556 normalized_version.as_deref(),
557 )
558 });
559
560 (
561 normalized_namespace,
562 normalized_name,
563 normalized_version,
564 purl,
565 )
566}
567
568fn extract_root_package_identity(
569 json: &Value,
570 root_name: String,
571 root_version: String,
572) -> (String, String) {
573 let root_package = json
574 .get(FIELD_PACKAGES)
575 .and_then(|value| value.as_object())
576 .and_then(|packages| packages.get(""))
577 .and_then(|value| value.as_object());
578
579 let name = non_empty_string(&root_name).or_else(|| {
580 root_package
581 .and_then(|package| package.get(FIELD_NAME))
582 .and_then(|value| value.as_str())
583 .map(str::to_string)
584 .filter(|value| !value.trim().is_empty())
585 });
586 let version = non_empty_string(&root_version).or_else(|| {
587 root_package
588 .and_then(|package| package.get(FIELD_VERSION))
589 .and_then(|value| value.as_str())
590 .map(str::to_string)
591 .filter(|value| !value.trim().is_empty())
592 });
593
594 (name.unwrap_or_default(), version.unwrap_or_default())
595}
596
597fn non_empty_string(value: &str) -> Option<String> {
598 let trimmed = value.trim();
599 if trimmed.is_empty() {
600 None
601 } else {
602 Some(trimmed.to_string())
603 }
604}
605
606fn collect_root_dependency_names(
607 value: Option<&Value>,
608 root_deps: &mut std::collections::HashSet<String>,
609) {
610 if let Some(entries) = value.and_then(|value| value.as_object()) {
611 for key in entries.keys().take(MAX_ITERATION_COUNT) {
612 root_deps.insert(key.clone());
613 }
614 }
615}
616
617fn is_direct_dependency_path(package_path: &str) -> bool {
618 let node_modules_count = package_path.matches("node_modules/").count();
619
620 match node_modules_count {
621 0 => true,
622 1 => package_path.starts_with("node_modules/") || package_path.starts_with(".pnpm/"),
623 _ => false,
624 }
625}
626
627fn parse_integrity_field(integrity: Option<&str>) -> (Option<String>, Option<String>) {
630 let integrity = match integrity {
631 Some(i) => i,
632 None => return (None, None),
633 };
634
635 match parse_sri(integrity) {
636 Some((algo, hex_digest)) => match algo.as_str() {
637 "sha1" => (Some(hex_digest), None),
638 "sha512" => (None, Some(hex_digest)),
639 _ => (None, None),
640 },
641 None => (None, None),
642 }
643}
644
645fn parse_resolved_url(url: &str) -> Option<String> {
648 if let Some(hash_pos) = url.rfind('#') {
650 let hash = &url[hash_pos + 1..];
651 if hash.len() == 40 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
653 return Some(hash.to_string());
654 }
655 }
656 None
657}
658
659fn determine_scope(
662 is_dev: bool,
663 is_dev_optional: bool,
664 is_optional: bool,
665) -> (&'static str, bool, bool) {
666 if is_dev || is_dev_optional {
667 ("devDependencies", false, true)
668 } else if is_optional {
669 ("dependencies", true, true)
670 } else {
671 ("dependencies", true, false)
672 }
673}
674
675fn parse_npm_alias_spec(version_spec: &str) -> Option<(String, String, String)> {
676 let aliased_spec = version_spec.strip_prefix("npm:")?;
677 let (aliased_name, constraint) = aliased_spec.rsplit_once('@')?;
678 let (namespace, name) = extract_namespace_and_name(aliased_name);
679
680 if name.is_empty() || constraint.trim().is_empty() {
681 None
682 } else {
683 Some((namespace, name, constraint.to_string()))
684 }
685}
686
687fn is_exact_version(version: &str) -> bool {
688 let version = version.trim();
689
690 if version.is_empty() {
691 return false;
692 }
693
694 if version.starts_with('~')
695 || version.starts_with('^')
696 || version.starts_with('>')
697 || version.starts_with('<')
698 || version.starts_with('=')
699 || version.starts_with('*')
700 || version.contains("||")
701 || version.contains(" - ")
702 {
703 return false;
704 }
705
706 !is_non_version_dependency(version)
707}
708
709fn is_non_version_dependency(version: &str) -> bool {
710 let version = version.trim();
711
712 version.starts_with("http://")
713 || version.starts_with("https://")
714 || version.starts_with("git://")
715 || version.starts_with("git+ssh://")
716 || version.starts_with("git+http://")
717 || version.starts_with("git+https://")
718 || version.starts_with("git+file://")
719 || version.starts_with("git@")
720 || version.starts_with("file:")
721 || version.starts_with("link:")
722 || version.starts_with("github:")
723 || version.starts_with("gitlab:")
724 || version.starts_with("bitbucket:")
725 || version.starts_with("gist:")
726}
727
728fn non_version_download_url(version: &str, resolved: Option<&str>) -> Option<String> {
729 resolved
730 .map(str::to_string)
731 .or_else(|| match version.trim() {
732 version if version.starts_with("http://") || version.starts_with("https://") => {
733 Some(version.to_string())
734 }
735 _ => None,
736 })
737}
738
739#[allow(clippy::too_many_arguments)]
740fn build_npm_dependency(
741 package_name: &str,
742 version: String,
743 is_dev: bool,
744 is_dev_optional: bool,
745 is_optional: bool,
746 resolved: Option<String>,
747 integrity: Option<&str>,
748 is_direct: bool,
749 from: Option<&str>,
750 in_bundle: bool,
751 nested_deps: Vec<Dependency>,
752) -> Dependency {
753 let (dep_namespace, dep_name) = extract_namespace_and_name(package_name);
754 let dep_namespace = truncate_field(dep_namespace);
755 let dep_name = truncate_field(dep_name);
756 let (scope, is_runtime, is_optional_flag) =
757 determine_scope(is_dev, is_dev_optional, is_optional);
758
759 let alias_spec = parse_npm_alias_spec(&version);
760 let (purl_namespace, purl_name, resolved_version, is_pinned, dep_purl, download_url) =
761 if let Some((alias_namespace, alias_name, alias_constraint)) = alias_spec.clone() {
762 let alias_namespace = truncate_field(alias_namespace);
763 let alias_name = truncate_field(alias_name);
764 let alias_constraint = truncate_field(alias_constraint);
765 let is_pinned = is_exact_version(&alias_constraint);
766 let dep_purl = create_purl(
767 &alias_namespace,
768 &alias_name,
769 is_pinned.then_some(alias_constraint.as_str()),
770 );
771 let download_url = non_version_download_url(&alias_constraint, resolved.as_deref());
772
773 (
774 alias_namespace,
775 alias_name,
776 alias_constraint,
777 is_pinned,
778 dep_purl,
779 download_url,
780 )
781 } else {
782 let is_pinned = is_exact_version(&version);
783 let dep_purl = create_purl(
784 &dep_namespace,
785 &dep_name,
786 is_pinned.then_some(version.as_str()),
787 );
788 let download_url = non_version_download_url(&version, resolved.as_deref());
789
790 (
791 dep_namespace.clone(),
792 dep_name.clone(),
793 version.clone(),
794 is_pinned,
795 dep_purl,
796 download_url,
797 )
798 };
799
800 let (sha1_from_integrity, sha512_from_integrity) = parse_integrity_field(integrity);
801 let sha1_from_url = resolved.as_deref().and_then(parse_resolved_url);
802 let sha1 = sha1_from_integrity.or(sha1_from_url);
803
804 let mut dep_extra_data = HashMap::new();
805 if let Some(from) = from {
806 dep_extra_data.insert("from".to_string(), Value::String(from.to_string()));
807 }
808 if in_bundle {
809 dep_extra_data.insert("inBundle".to_string(), Value::Bool(true));
810 }
811
812 let resolved_package = ResolvedPackage {
813 primary_language: Some("JavaScript".to_string()),
814 download_url,
815 sha1: sha1.and_then(|h| Sha1Digest::from_hex(&h).ok()),
816 sha256: None,
817 sha512: sha512_from_integrity.and_then(|h| Sha512Digest::from_hex(&h).ok()),
818 md5: None,
819 is_virtual: true,
820 extra_data: None,
821 dependencies: nested_deps,
822 repository_homepage_url: None,
823 repository_download_url: None,
824 api_data_url: None,
825 datasource_id: Some(DatasourceId::NpmPackageLockJson),
826 purl: None,
827 ..ResolvedPackage::new(
828 NpmLockParser::PACKAGE_TYPE,
829 purl_namespace,
830 purl_name,
831 resolved_version,
832 )
833 };
834
835 Dependency {
836 purl: dep_purl,
837 extracted_requirement: Some(truncate_field(version)),
838 scope: Some(scope.to_string()),
839 is_runtime: Some(is_runtime),
840 is_optional: Some(is_optional_flag),
841 is_pinned: Some(is_pinned),
842 is_direct: Some(is_direct),
843 resolved_package: Some(Box::new(resolved_package)),
844 extra_data: (!dep_extra_data.is_empty()).then_some(dep_extra_data),
845 }
846}
847
848fn build_link_dependency(
849 package_name: &str,
850 is_dev: bool,
851 is_dev_optional: bool,
852 is_optional: bool,
853 resolved: Option<String>,
854 is_direct: bool,
855) -> Dependency {
856 let (dep_namespace, dep_name) = extract_namespace_and_name(package_name);
857 let dep_namespace = truncate_field(dep_namespace);
858 let dep_name = truncate_field(dep_name);
859 let (scope, is_runtime, is_optional_flag) =
860 determine_scope(is_dev, is_dev_optional, is_optional);
861 let mut extra_data = HashMap::from([("link".to_string(), Value::Bool(true))]);
862
863 if let Some(resolved) = &resolved {
864 extra_data.insert("resolved".to_string(), Value::String(resolved.clone()));
865 }
866
867 Dependency {
868 purl: create_purl(&dep_namespace, &dep_name, None),
869 extracted_requirement: resolved.map(truncate_field),
870 scope: Some(scope.to_string()),
871 is_runtime: Some(is_runtime),
872 is_optional: Some(is_optional_flag),
873 is_pinned: Some(false),
874 is_direct: Some(is_direct),
875 resolved_package: None,
876 extra_data: Some(extra_data),
877 }
878}
879
880crate::register_parser!(
881 "npm package-lock.json lockfile",
882 &[
883 "**/package-lock.json",
884 "**/.package-lock.json",
885 "**/npm-shrinkwrap.json"
886 ],
887 "npm",
888 "JavaScript",
889 Some("https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json"),
890);