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