Skip to main content

socket_patch_core/utils/
enumerate.rs

1use std::path::Path;
2
3use crate::crawlers::types::{CrawledPackage, CrawlerOptions};
4use crate::crawlers::NpmCrawler;
5
6/// Type alias for backward compatibility with the TypeScript codebase.
7pub type EnumeratedPackage = CrawledPackage;
8
9/// Enumerate all packages in a `node_modules` directory.
10///
11/// This is a convenience wrapper around `NpmCrawler::crawl_all` that creates
12/// a crawler with default options rooted at the given `cwd`.
13pub async fn enumerate_node_modules(cwd: &Path) -> Vec<CrawledPackage> {
14    let crawler = NpmCrawler::new();
15    let options = CrawlerOptions {
16        cwd: cwd.to_path_buf(),
17        global: false,
18        global_prefix: None,
19        batch_size: 100,
20    };
21    crawler.crawl_all(&options).await
22}
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27
28    #[tokio::test]
29    async fn test_enumerate_empty_dir() {
30        let dir = tempfile::tempdir().unwrap();
31        let packages = enumerate_node_modules(dir.path()).await;
32        assert!(packages.is_empty());
33    }
34
35    #[tokio::test]
36    async fn test_enumerate_with_packages() {
37        let dir = tempfile::tempdir().unwrap();
38        let nm = dir.path().join("node_modules");
39
40        // Create a simple package
41        let pkg_dir = nm.join("test-pkg");
42        tokio::fs::create_dir_all(&pkg_dir).await.unwrap();
43        tokio::fs::write(
44            pkg_dir.join("package.json"),
45            r#"{"name": "test-pkg", "version": "1.0.0"}"#,
46        )
47        .await
48        .unwrap();
49
50        // Create a scoped package
51        let scoped_dir = nm.join("@scope").join("my-lib");
52        tokio::fs::create_dir_all(&scoped_dir).await.unwrap();
53        tokio::fs::write(
54            scoped_dir.join("package.json"),
55            r#"{"name": "@scope/my-lib", "version": "2.0.0"}"#,
56        )
57        .await
58        .unwrap();
59
60        let packages = enumerate_node_modules(dir.path()).await;
61        assert_eq!(packages.len(), 2);
62
63        let purls: Vec<&str> = packages.iter().map(|p| p.purl.as_str()).collect();
64        assert!(purls.contains(&"pkg:npm/test-pkg@1.0.0"));
65        assert!(purls.contains(&"pkg:npm/@scope/my-lib@2.0.0"));
66    }
67
68    #[tokio::test]
69    async fn test_enumerate_deduplicates() {
70        let dir = tempfile::tempdir().unwrap();
71        let nm = dir.path().join("node_modules");
72
73        // Create package at top level
74        let pkg1 = nm.join("foo");
75        tokio::fs::create_dir_all(&pkg1).await.unwrap();
76        tokio::fs::write(
77            pkg1.join("package.json"),
78            r#"{"name": "foo", "version": "1.0.0"}"#,
79        )
80        .await
81        .unwrap();
82
83        // Create same package nested inside another
84        let pkg2 = nm.join("bar");
85        tokio::fs::create_dir_all(&pkg2).await.unwrap();
86        tokio::fs::write(
87            pkg2.join("package.json"),
88            r#"{"name": "bar", "version": "2.0.0"}"#,
89        )
90        .await
91        .unwrap();
92        let nested_foo = pkg2.join("node_modules").join("foo");
93        tokio::fs::create_dir_all(&nested_foo).await.unwrap();
94        tokio::fs::write(
95            nested_foo.join("package.json"),
96            r#"{"name": "foo", "version": "1.0.0"}"#,
97        )
98        .await
99        .unwrap();
100
101        let packages = enumerate_node_modules(dir.path()).await;
102        // foo@1.0.0 should be deduplicated
103        let foo_count = packages
104            .iter()
105            .filter(|p| p.purl == "pkg:npm/foo@1.0.0")
106            .count();
107        assert_eq!(foo_count, 1);
108    }
109}