Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 52 additions & 20 deletions crates/socket-patch-core/src/crawlers/npm_crawler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1882,7 +1882,9 @@ const DENO_NPM_REGISTRY_MAX_DEPTH: usize = 8;
/// [`is_deno_npm_cache_root`]), so these are global roots beside the npm,
/// pnpm, yarn and bun `node_modules`. Below a registry dir only the
/// non-version subdirs of its children are walked (a nested registry
/// can sit there, not a package's copy), and `jsr.io` is skipped: it is the
/// can sit there, not a package's copy); a package dir whose other
/// subdirs hold packages is itself a registry one path segment down
/// (#1784). `jsr.io` is skipped: it is the
/// Deno crawler's JSR root, not an npm registry. Sorted, for a stable
/// scan order.
fn deno_npm_cache_roots() -> Vec<PathBuf> {
Expand Down Expand Up @@ -1945,20 +1947,30 @@ fn deno_npm_registry_dirs(npm: &Path) -> Vec<PathBuf> {
// (`<host>/npm/<pkg>/` or `<host>/api/npm/<pkg>/` beside
// `<host>/<pkg>/`). A child that is not a package is walked
// like any other dir; one that is (a package named like
// the path's first segment, `<host>/api/`) holds only
// version dirs (named from a digit) of its own, so its
// other subdirs are walked.
for child in children.into_iter().filter(|name| !name.starts_with('@')) {
let child = dir.join(child);
if is_deno_package_dir(&child) {
next.extend(
sorted_subdirs_sync(&child)
.into_iter()
.filter(|name| !name.starts_with(|c: char| c.is_ascii_digit()))
.map(|name| child.join(name)),
);
} else {
// the path's first segment, `<host>/api/`) holds version
// dirs of its own, so only its other subdirs are walked.
// When those subdirs are packages themselves
// (`https://host/npm/` caches `<host>/npm/<pkg>/` beside
// the root registry's package `npm`), the package dir is
// also a registry (#1784), whose own version dirs (those
// of the outer package) are skipped.
for name in children.into_iter().filter(|name| !name.starts_with('@')) {
if is_deno_version_dir(&dir, &name) {
continue;
}
let child = dir.join(name);
if !is_deno_package_dir(&child) {
next.push(child);
continue;
}
let others: Vec<String> = sorted_subdirs_sync(&child)
.into_iter()
.filter(|name| !is_deno_version_dir(&child, name))
.collect();
if holds_deno_packages_among(&child, &others) {
next.push(child);
} else {
next.extend(others.into_iter().map(|name| child.join(name)));
}
}
roots.push(dir);
Expand Down Expand Up @@ -2003,8 +2015,13 @@ pub(super) fn is_deno_npm_cache_root(root: &Path) -> bool {
/// out the way Deno's npm cache lays one out ([`is_deno_package_dir`]),
/// stopping at the first that is.
fn holds_deno_packages(root: &Path) -> bool {
sorted_subdirs_sync(root).into_iter().any(|name| {
let dir = root.join(&name);
holds_deno_packages_among(root, &sorted_subdirs_sync(root))
}

/// [`holds_deno_packages`] over only the subdirs `names` of `root`.
fn holds_deno_packages_among(root: &Path, names: &[String]) -> bool {
names.iter().any(|name| {
let dir = root.join(name);
if name.starts_with('@') {
sorted_subdirs_sync(&dir)
.into_iter()
Expand All @@ -2015,6 +2032,16 @@ fn holds_deno_packages(root: &Path) -> bool {
})
}

/// Whether `parent/name` is a version dir of a package in Deno's npm
/// cache (`<version>/` or `<version>_<n>/`, named from a digit, with the
/// package's `package.json`) rather than a package dir: a package named
/// from a digit (`7zip-bin`) has no `package.json` of its own. Only a
/// digit-named dir is stat'ed.
fn is_deno_version_dir(parent: &Path, name: &str) -> bool {
name.starts_with(|c: char| c.is_ascii_digit())
&& parent.join(name).join("package.json").is_file()
}

/// Whether `dir` is a package dir of Deno's npm cache: it has no
/// `package.json` of its own (a `node_modules` package dir always does)
/// and holds Deno's cached packument (`registry.json`) or a version dir
Expand All @@ -2026,10 +2053,9 @@ fn is_deno_package_dir(dir: &Path) -> bool {
return false;
}
dir.join("registry.json").is_file()
|| sorted_subdirs_sync(dir).into_iter().any(|version| {
version.starts_with(|c: char| c.is_ascii_digit())
&& dir.join(version).join("package.json").is_file()
})
|| sorted_subdirs_sync(dir)
.into_iter()
.any(|version| is_deno_version_dir(dir, &version))
}

/// The copy index of the Deno npm cache dir `dir_name` when it holds
Expand Down Expand Up @@ -2066,6 +2092,12 @@ fn sorted_subdirs_sync(dir: &Path) -> Vec<String> {
fn deno_npm_cache_package_dirs(root: &Path) -> Vec<PathBuf> {
let mut dirs = Vec::new();
for name in sorted_subdirs_sync(root) {
// A registry that is also a package dir of the registry above it
// (#1784) holds that package's version dirs, which are not
// packages of its own.
if is_deno_version_dir(root, &name) {
continue;
}
if name.starts_with('@') {
let scope = root.join(&name);
dirs.extend(
Expand Down
78 changes: 78 additions & 0 deletions crates/socket-patch-core/tests/crawler_npm_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4712,6 +4712,84 @@ async fn global_mode_finds_one_segment_registry_under_root_registry() {
assert_eq!(deno_roots, vec![&host, &nested], "{roots:?}");
}

/// #1784: a one-segment path registry (`https://host/npm/` →
/// `<host>/npm/<pkg>/`) stays a root when the host-root registry has also
/// cached a package named like that segment (`<host>/npm/<version>/`),
/// whether the nested registry's packages are plain or scoped, and the
/// scan reports each registry's packages once, from its own dir.
#[tokio::test]
#[serial_test::serial]
async fn global_mode_finds_one_segment_registry_behind_same_named_package() {
struct DenoDirGuard(Option<std::ffi::OsString>);
impl Drop for DenoDirGuard {
fn drop(&mut self) {
match self.0.take() {
Some(v) => std::env::set_var("DENO_DIR", v),
None => std::env::remove_var("DENO_DIR"),
}
}
}
let _guard = DenoDirGuard(std::env::var_os("DENO_DIR"));
for nested_pkg in ["foo", "@scope/foo"] {
let tmp = tempfile::tempdir().unwrap();
let deno_dir = tmp.path().join("deno");
let host = deno_dir.join("npm").join("registry.example.com");
let nested = host.join("npm");
let npm_pkg = stage_deno_npm_pkg(&host, "npm", "10.9.0", "10.9.0").await;
// The outer package's own content: a nested manifest and a
// fixture tree laid out like a registry, neither of which is a
// package or registry of `<host>/npm`.
stage_npm_pkg(&npm_pkg.join("node_modules"), "abbrev", "2.0.0").await;
tokio::fs::create_dir_all(npm_pkg.join("dist"))
.await
.unwrap();
tokio::fs::write(
npm_pkg.join("dist").join("package.json"),
r#"{"name":"npm-dist","version":"0.0.1"}"#,
)
.await
.unwrap();
stage_deno_npm_pkg(&npm_pkg.join("fixtures"), "bar", "2.0.0", "2.0.0").await;
let foo = stage_deno_npm_pkg(&nested, nested_pkg, "1.0.0", "1.0.0").await;

std::env::set_var("DENO_DIR", &deno_dir);
let opts = CrawlerOptions {
cwd: tmp.path().to_path_buf(),
global: true,
global_prefix: None,
};
let roots = NpmCrawler.get_node_modules_paths(&opts).await.unwrap();
let deno_roots: Vec<&PathBuf> = roots.iter().filter(|r| r.starts_with(&deno_dir)).collect();
assert_eq!(deno_roots, vec![&host, &nested], "{nested_pkg}: {roots:?}");

let mut crawled: Vec<(String, PathBuf)> = NpmCrawler
.crawl_all(&opts)
.await
.into_iter()
.filter(|p| p.path.starts_with(&deno_dir))
.map(|p| (p.purl, p.path))
.collect();
crawled.sort();
let foo_purl = format!("pkg:npm/{nested_pkg}@1.0.0");
let mut expected = vec![
("pkg:npm/npm@10.9.0".to_string(), npm_pkg.clone()),
(foo_purl.clone(), foo.clone()),
];
expected.sort();
assert_eq!(crawled, expected, "{nested_pkg}");

let found = NpmCrawler
.find_by_purls(&nested, &[foo_purl.clone()])
.await
.unwrap();
assert_eq!(
found_paths(&found, &foo_purl),
vec![foo.clone()],
"{nested_pkg}"
);
}
}

/// #444: a `--global-prefix` naming `$DENO_DIR` or `$DENO_DIR/npm` (a parent
/// of Deno's npm cache registry dirs) expands to every registry dir below
/// it, the same ones `-g` finds, so the scan sees the packages
Expand Down
Loading