1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use tracing::instrument;
6
7#[derive(Debug, Error)]
8#[error("package-lock.json error")]
9pub enum PackageLockJsonError {
10 #[error("Error parsing file: {0}")]
11 ParseError(#[from] serde_json::Error),
12}
13
14#[derive(Debug, Serialize, Deserialize, Default, Clone, Eq, PartialEq)]
15pub struct PackageLockJson {
16 pub name: String,
17 pub version: Option<String>,
18 #[serde(rename = "lockfileVersion")]
19 pub lockfile_version: u32,
20 pub dependencies: Option<HashMap<String, V1Dependency>>,
21 #[serde(deserialize_with = "deserialize_packages", default)]
22 pub packages: Option<HashMap<String, V2Dependency>>,
23}
24
25#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Default)]
26pub struct V1Dependency {
27 pub version: String,
28 pub resolved: Option<String>,
29 pub integrity: Option<String>,
30 #[serde(default)]
31 pub bundled: bool,
32 #[serde(rename = "dev", default)]
33 pub is_dev: bool,
34 #[serde(rename = "optional", default)]
35 pub is_optional: bool,
36 pub requires: Option<HashMap<String, String>>,
37 pub dependencies: Option<HashMap<String, V1Dependency>>,
38}
39
40#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Default)]
41pub struct V2Dependency {
42 pub version: String,
43 pub name: Option<String>,
44 pub resolved: Option<String>,
45 pub integrity: Option<String>,
46 #[serde(default)]
47 pub bundled: bool,
48 #[serde(rename = "dev", default)]
49 pub is_dev: bool,
50 #[serde(rename = "optional", default)]
51 pub is_optional: bool,
52 #[serde(rename = "devOptional", default)]
53 pub is_dev_optional: bool,
54 #[serde(rename = "inBundle", default)]
55 pub is_in_bundle: bool,
56 #[serde(rename = "hasInstallScript", default)]
57 pub has_install_script: bool,
58 #[serde(rename = "hasShrinkwrap", default)]
59 pub has_shrink_wrap: bool,
60 pub dependencies: Option<HashMap<String, String>>,
61 #[serde(rename = "devDependencies")]
62 pub dev_dependencies: Option<HashMap<String, String>>,
63 #[serde(rename = "optionalDependencies")]
64 pub optional_dependencies: Option<HashMap<String, String>>,
65 #[serde(rename = "peerDependencies")]
66 pub peer_dependencies: Option<HashMap<String, String>>,
67 pub license: Option<String>,
68 pub engines: Option<HashMap<String, String>>,
69 pub bin: Option<HashMap<String, String>>,
70}
71
72#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
73pub struct SimpleDependency {
74 pub name: String,
75 pub version: String,
76 pub is_dev: bool,
77 pub is_optional: bool,
78}
79
80#[instrument(skip(content))]
83pub fn parse(
84 content: impl Into<String> + std::fmt::Debug,
85) -> Result<PackageLockJson, PackageLockJsonError> {
86 let mut json: PackageLockJson = serde_json::from_str(&content.into())?;
87 if let (Some(dependencies), Some(packages)) =
90 (json.dependencies.as_mut(), json.packages.as_ref())
91 {
92 for (name, dependency) in dependencies {
93 if dependency.version.starts_with("file:") {
94 if let Some(pkg) = packages.get(name) {
95 dependency.version = pkg.version.clone();
96 }
97 }
98 }
99 }
100 Ok(json)
101}
102
103#[instrument(skip(content))]
107pub fn parse_dependencies(
108 content: impl Into<String> + std::fmt::Debug,
109) -> Result<Vec<SimpleDependency>, PackageLockJsonError> {
110 let json = parse(content)?;
111 let mut entries = Vec::new();
112 if let Some(dependencies) = json.dependencies {
113 for (name, dependency) in dependencies {
114 entries.push(SimpleDependency {
115 name,
116 version: dependency.version,
117 is_dev: dependency.is_dev,
118 is_optional: dependency.is_optional,
119 });
120 }
121 } else if let Some(packages) = json.packages {
122 for (name, dependency) in packages {
123 entries.push(SimpleDependency {
124 name,
125 version: dependency.version,
126 is_dev: dependency.is_dev,
127 is_optional: dependency.is_optional,
128 });
129 }
130 }
131 Ok(entries)
132}
133
134fn deserialize_packages<'de, D>(
135 deserializer: D,
136) -> Result<Option<HashMap<String, V2Dependency>>, D::Error>
137where
138 D: serde::Deserializer<'de>,
139{
140 let value: Option<HashMap<String, serde_json::Value>> =
141 serde::Deserialize::deserialize(deserializer)?;
142 if let Some(package) = value {
143 let mut packages = HashMap::new();
144 for (key, mut value) in package {
145 if key.is_empty() {
146 tracing::info!("Skipping package information in packages.");
148 continue;
149 }
150 if let Some(engines) = value.get("engines").and_then(serde_json::Value::as_array) {
153 tracing::warn!(
154 "Found engines as an array instead of an object. Fixing it. ({})",
155 key
156 );
157 if engines.is_empty() {
158 value["engines"] = serde_json::Value::Null;
159 } else {
160 let mut new_engines = HashMap::new();
161 for engine in engines {
162 let engine = engine.as_str().unwrap();
163 let (name, version) =
164 engine.split_once(' ').unwrap_or(("not_found", "not_found"));
165 new_engines.insert(name, version);
166 }
167 value["engines"] = serde_json::value::to_value(new_engines).unwrap();
168 }
169 }
170
171 let vclone = value.clone();
172
173 let package = serde_json::from_value::<V2Dependency>(value);
174 match package {
175 Ok(package) => {
176 let pattern = "node_modules/";
177 if key.starts_with(pattern) {
178 if !key.contains("/node_modules/") {
179 let key = key.replace(pattern, "");
181 packages.insert(key, package);
182 }
183 } else {
184 if let Some(ref name) = package.name {
186 packages.insert(name.clone(), package);
190 } else {
191 packages.insert(key, package);
192 }
193 }
194 }
195 Err(e) => {
196 tracing::error!(
199 "Could not parse this dependency: {:?}, ERROR: {}",
200 vclone,
201 e
202 );
203 continue;
204 }
205 };
206 }
207 Ok(Some(packages))
208 } else {
209 Ok(None)
210 }
211}
212
213#[cfg(test)]
214mod tests {
215
216 use super::*;
217
218 fn expected_v1() -> V1Dependency {
219 V1Dependency{
220 version : "7.18.6".to_string(),
221 resolved: Some("https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz".to_string()),
222 integrity: Some("sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==".to_string()),
223 bundled: false,
224 is_dev: true,
225 is_optional: false,
226 requires: Some(HashMap::from([("js-tokens".to_string(), "^4.0.0".to_string()), ("chalk".to_string(), "^2.0.0".to_string()),("@babel/helper-validator-identifier".to_string(), "^7.18.6".to_string())])),
227 dependencies: Some(HashMap::from([("js-tokens".to_string(), V1Dependency {
228 version: "4.0.0".to_string(),
229 resolved: Some("https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz".to_string()),
230 integrity: Some("sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==".to_string()),
231 is_dev: true,
232 bundled: false,
233 ..V1Dependency::default()
234 })]
235 ))
236 }
237 }
238
239 fn expected_v2() -> V2Dependency {
240 V2Dependency{
241 version : "7.18.6".to_string(),
242 resolved: Some("https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz".to_string()),
243 integrity: Some("sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==".to_string()),
244 bundled: false,
245 is_dev: true,
246 is_optional: false,
247 dependencies: Some(HashMap::from([("js-tokens".to_string(), "^4.0.0".to_string()), ("chalk".to_string(), "^2.0.0".to_string()),("@babel/helper-validator-identifier".to_string(), "^7.18.6".to_string())])),
248 engines: Some(HashMap::from([("node".to_string(), ">=6.9.0".to_string())])),
249 ..V2Dependency::default()
250 }
251 }
252
253 #[test]
254 fn works_without_version() {
255 let content = std::fs::read_to_string("tests/cool-project/package-lock.json").unwrap();
256 let lock_file = parse(content).unwrap();
257 assert_eq!(lock_file.name, "cool-project");
258 assert!(lock_file.version.is_none());
259 }
260
261 #[test]
262 fn cool_project_works() {
263 let content = std::fs::read_to_string("tests/cool-project/package-lock.json").unwrap();
264 let lock_file = parse(content).unwrap();
265 assert_eq!(lock_file.name, "cool-project");
266 assert!(lock_file.version.is_none());
267 assert_eq!(lock_file.lockfile_version, 2);
268
269 assert!(lock_file.dependencies.is_some());
270 assert!(lock_file.packages.is_some());
271
272 let packages = lock_file.packages.unwrap();
273 let cool = packages.get("cool-project").unwrap();
274 assert_eq!(cool.name, Some("cool-project".to_string()));
275 assert_eq!(cool.version, "23.1.21".to_string());
276
277 let dependencies = lock_file.dependencies.unwrap();
278 let cool = dependencies.get("cool-project").unwrap();
279 assert_eq!(cool.version, "23.1.21".to_string());
280 }
281
282 #[test]
283 fn parse_moon_workspace_dependencies_works() {
284 let content = std::fs::read_to_string("tests/workspace/moon/package-lock.json").unwrap();
285 let lock_file = parse(content).unwrap();
286 assert_eq!(lock_file.name, "moon-examples");
287 assert_eq!(lock_file.version, Some("1.2.3".to_string()));
288 assert_eq!(lock_file.lockfile_version, 3);
289
290 assert!(lock_file.dependencies.is_none());
291 assert!(lock_file.packages.is_some());
292
293 let packages = lock_file.packages.unwrap();
294
295 let yaml = packages.get("yaml").unwrap();
296 let expected_yaml = V2Dependency {
297 version: "2.2.2".to_string(),
298 resolved: Some("https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz".to_string()),
299 integrity: Some("sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==".to_string()),
300 is_dev: true,
301 engines: Some(HashMap::from([("node".to_string(), ">= 14".to_string())])),
302 ..V2Dependency::default()
303 };
304 assert_eq!(yaml, &expected_yaml);
305
306 let libnpmdiff = packages.get("workspaces/libnpmdiff").unwrap();
308 assert_eq!(libnpmdiff.version, "5.0.17".to_string());
309 assert_eq!(libnpmdiff.license, Some("ISC".to_string()));
310 assert!(libnpmdiff.dependencies.is_some());
311 let dependencies = libnpmdiff.dependencies.as_ref().unwrap();
312 assert!(dependencies.contains_key("pacote"));
313 assert!(dependencies.contains_key("tar"));
314 }
315
316 #[test]
317 fn parse_v2_workspace_dependencies_works() {
318 let content = std::fs::read_to_string("tests/workspace/v2/package-lock.json").unwrap();
319 let lock_file = parse(content).unwrap();
320 assert_eq!(lock_file.name, "test-node-npm");
321 assert_eq!(lock_file.version, Some("1.0.0".to_string()));
322 assert_eq!(lock_file.lockfile_version, 2);
323
324 assert!(lock_file.dependencies.is_some());
325 assert!(lock_file.packages.is_some());
326
327 let packages = lock_file.packages.unwrap();
328
329 let test_node_npm_base = packages.get("test-node-npm-base").unwrap();
330 let expected_base = V2Dependency {
331 version: "1.0.0".to_string(),
332 name: Some("test-node-npm-base".to_string()),
333 dependencies: Some(HashMap::from([("react".to_string(), "17.0.0".to_string())])),
334 ..V2Dependency::default()
335 };
336 assert_eq!(test_node_npm_base, &expected_base);
337
338 let base = packages.get("base");
340 assert!(base.is_none());
341
342 let dependencies = lock_file.dependencies.unwrap();
344 let test_node_npm_v1 = dependencies.get("test-node-npm-base").unwrap();
345 assert_eq!(
346 test_node_npm_v1,
347 &V1Dependency {
348 version: "1.0.0".to_string(),
349 requires: Some(HashMap::from([("react".to_string(), "17.0.0".to_string())])),
350 ..V1Dependency::default()
351 }
352 );
353 }
354
355 #[test]
356 fn parse_v3_workspace_dependencies_works() {
357 let content = std::fs::read_to_string("tests/workspace/v3/package-lock.json").unwrap();
358 let lock_file = parse(content).unwrap();
359 assert_eq!(lock_file.name, "kk");
360 assert_eq!(lock_file.version, Some("1.0.0".to_string()));
361 assert_eq!(lock_file.lockfile_version, 3);
362
363 assert!(lock_file.dependencies.is_none());
364 assert!(lock_file.packages.is_some());
365
366 let packages = lock_file.packages.unwrap();
367
368 let liba = packages.get("liba").unwrap();
370 let expected_liba = V2Dependency {
371 version: "1.0.0".to_string(),
372 resolved: None,
373 integrity: None,
374 bundled: false,
375 is_dev: false,
376 is_optional: false,
377 dependencies: Some(HashMap::from([("libb2".to_string(), "*".to_string())])),
378 license: Some("ISC".to_string()),
379 engines: None,
380 ..V2Dependency::default()
381 };
382 assert_eq!(liba, &expected_liba);
383
384 let libb = packages.get("libb");
386 assert!(libb.is_none());
387
388 let libb2 = packages.get("libb2").unwrap();
390 let expected_libb2 = V2Dependency {
391 name: Some("libb2".to_string()),
392 version: "1.0.0".to_string(),
393 resolved: None,
394 integrity: None,
395 bundled: false,
396 is_dev: false,
397 is_optional: false,
398 dependencies: None,
399 license: Some("ISC".to_string()),
400 engines: None,
401 ..V2Dependency::default()
402 };
403 assert_eq!(libb2, &expected_libb2);
404 }
405
406 #[test]
407 fn parse_v1_from_file_works() {
408 let content = std::fs::read_to_string("tests/v1/package-lock.json").unwrap();
409 let lock_file = parse(content).unwrap();
410 assert_eq!(lock_file.name, "cxtl");
411 assert_eq!(lock_file.version, Some("1.0.0".to_string()));
412 assert_eq!(lock_file.lockfile_version, 1);
413
414 assert!(lock_file.dependencies.is_some());
415 assert!(lock_file.packages.is_none());
416
417 let dependencies = lock_file.dependencies.unwrap();
418 let babel_highlight = dependencies.get("@babel/highlight").unwrap();
419
420 let expected = expected_v1();
421
422 assert_eq!(babel_highlight, &expected);
423 }
424
425 #[test]
426 fn parse_v2_from_file_works() {
427 let content = std::fs::read_to_string("tests/v2/package-lock.json").unwrap();
428 let lock_file = parse(content).unwrap();
429 assert_eq!(lock_file.name, "cxtl");
430 assert_eq!(lock_file.version, Some("1.0.0".to_string()));
431 assert_eq!(lock_file.lockfile_version, 2);
432
433 assert!(lock_file.dependencies.is_some());
434 assert!(lock_file.packages.is_some());
435
436 let dependencies = lock_file.dependencies.unwrap();
438 let babel_highlight = dependencies.get("@babel/highlight").unwrap();
439
440 let expected = expected_v1();
441 assert_eq!(babel_highlight, &expected);
442
443 let packages = lock_file.packages.unwrap();
445 let babel_highlight = packages.get("@babel/highlight").unwrap();
446
447 let expected = expected_v2();
448
449 assert_eq!(babel_highlight, &expected);
450 }
451
452 #[test]
453 fn parse_v3_from_file_works() {
454 let content = std::fs::read_to_string("tests/v3/package-lock.json").unwrap();
455 let lock_file = parse(content).unwrap();
456 assert_eq!(lock_file.name, "cxtl");
457 assert_eq!(lock_file.version, Some("1.0.0".to_string()));
458 assert_eq!(lock_file.lockfile_version, 3);
459
460 assert!(lock_file.dependencies.is_none());
461 assert!(lock_file.packages.is_some());
462
463 let packages = lock_file.packages.unwrap();
464 let babel_highlight = packages.get("@babel/highlight").unwrap();
465
466 let expected = expected_v2();
467
468 assert_eq!(babel_highlight, &expected);
469 }
470
471 #[test]
472 fn deserialize_packages_works() {
473 let content = r#"{
474 "node_modules/extsprintf": {
475 "version": "1.3.0",
476 "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
477 "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
478 "dev": true,
479 "engines": [
480 "node >=0.6.0"
481 ]
482 }
483 }"#;
484
485 let mut deserializer = serde_json::Deserializer::from_str(content);
486 let packages = deserialize_packages(&mut deserializer).unwrap().unwrap();
487 let package = packages.get("extsprintf").unwrap();
489 assert_eq!(package.version, "1.3.0");
490 assert!(package.is_dev);
491 assert_eq!(
492 package.engines,
493 Some(HashMap::from([("node".to_string(), ">=0.6.0".to_string())]))
494 );
495 }
496
497 #[test]
498 fn parse_entries_v1_works() {
499 let content = std::fs::read_to_string("tests/v1/package-lock.json").unwrap();
500 let mut dependencies = parse_dependencies(content).unwrap();
501 dependencies.sort();
502
503 let first = dependencies.first().unwrap();
504 assert_eq!(first.name, "@babel/code-frame");
505 assert_eq!(first.version, "7.18.6");
506 assert!(first.is_dev);
507 assert!(!first.is_optional);
508 }
509
510 #[test]
511 fn parse_entries_v2_works() {
512 let content = std::fs::read_to_string("tests/v3/package-lock.json").unwrap();
513 let mut dependencies = parse_dependencies(content).unwrap();
514 dependencies.sort();
515
516 let first = dependencies.first().unwrap();
517 assert_eq!(first.name, "@babel/code-frame");
518 assert_eq!(first.version, "7.18.6");
519 assert!(first.is_dev);
520 assert!(!first.is_optional);
521 }
522}