use crate::Error;
use std::path::Path;
pub fn sweep_stale_tmp_dirs(virtual_store: &Path) {
let Ok(entries) = std::fs::read_dir(virtual_store) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.starts_with(".tmp-") {
continue;
}
let rest = &name[".tmp-".len()..];
let Some((pid_str, _rest)) = rest.split_once('-') else {
continue;
};
if pid_str.chars().any(|c| !c.is_ascii_digit()) {
continue;
}
if pid_str == std::process::id().to_string() {
continue;
}
let _ = remove_dir_all_with_retry(&entry.path());
}
}
pub fn remove_dir_all_with_retry(path: &Path) -> std::io::Result<()> {
#[cfg(not(windows))]
{
std::fs::remove_dir_all(path)
}
#[cfg(windows)]
{
use std::io::ErrorKind;
let mut delay_ms = 50u64;
for attempt in 0..10 {
match std::fs::remove_dir_all(path) {
Ok(()) => return Ok(()),
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
Err(e) => {
let retriable =
matches!(e.kind(), ErrorKind::PermissionDenied | ErrorKind::Other)
|| e.raw_os_error() == Some(32);
if !retriable || attempt == 9 {
return Err(e);
}
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
delay_ms = (delay_ms * 2).min(2000);
}
}
}
Ok(())
}
}
pub fn is_physical_importer(importer_path: &str) -> bool {
importer_path == "." || !importer_path.contains("/node_modules/")
}
pub(crate) fn remove_hidden_hoist_tree(path: &Path) {
match std::fs::symlink_metadata(path) {
Ok(md) if md.file_type().is_symlink() => {
let _ = std::fs::remove_file(path);
}
Ok(_) => {
let _ = std::fs::remove_dir_all(path);
}
Err(_) => {}
}
}
pub(crate) fn try_remove_entry(path: &Path) {
let _ = std::fs::remove_dir_all(path);
let _ = std::fs::remove_file(path);
}
pub fn mkdirp(dir: &Path) -> Result<(), Error> {
xx::file::mkdirp(dir).map_err(|e| Error::Xx(e.to_string()))
}
#[derive(Copy, Clone)]
pub(crate) enum EntryState {
Fresh,
Missing,
Stale,
}
pub(crate) fn sweep_stale_top_level_entries(
nm: &Path,
preserve: &std::collections::HashSet<&str>,
aube_dir_leaf: Option<&std::ffi::OsStr>,
) {
let scope_prefixes: std::collections::HashSet<&str> = preserve
.iter()
.filter_map(|n| n.split_once('/').map(|(scope, _)| scope))
.collect();
let Ok(entries) = std::fs::read_dir(nm) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with('.') {
continue;
}
if aube_dir_leaf == Some(name.as_os_str()) {
continue;
}
if preserve.contains(name_str.as_ref()) {
continue;
}
if scope_prefixes.contains(name_str.as_ref()) {
let scope_dir = entry.path();
if let Ok(inner) = std::fs::read_dir(&scope_dir) {
for inner_entry in inner.flatten() {
let inner_name = inner_entry.file_name();
let full = format!("{}/{}", name_str, inner_name.to_string_lossy());
if !preserve.contains(full.as_str()) {
try_remove_entry(&inner_entry.path());
}
}
}
if std::fs::read_dir(&scope_dir)
.map(|mut d| d.next().is_none())
.unwrap_or(false)
{
let _ = std::fs::remove_dir(&scope_dir);
}
continue;
}
try_remove_entry(&entry.path());
}
}
pub(crate) fn sweep_dead_hidden_hoist_entries(hidden: &Path) {
let Ok(entries) = std::fs::read_dir(hidden) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with('.') {
continue;
}
let path = entry.path();
if name_str.starts_with('@') {
match std::fs::symlink_metadata(&path) {
Ok(md) if md.is_dir() && !md.file_type().is_symlink() => {
sweep_dead_hidden_hoist_scope(&path);
if std::fs::read_dir(&path)
.map(|mut d| d.next().is_none())
.unwrap_or(false)
{
let _ = std::fs::remove_dir(&path);
}
}
Ok(_) => sweep_dead_hidden_hoist_entry(&path),
Err(_) => {}
}
continue;
}
sweep_dead_hidden_hoist_entry(&path);
}
}
fn sweep_dead_hidden_hoist_scope(scope_dir: &Path) {
let Ok(entries) = std::fs::read_dir(scope_dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy().starts_with('.') {
continue;
}
sweep_dead_hidden_hoist_entry(&entry.path());
}
}
fn sweep_dead_hidden_hoist_entry(path: &Path) {
match std::fs::symlink_metadata(path) {
Ok(md) if md.file_type().is_symlink() && path.exists() => {}
Ok(md) if md.file_type().is_symlink() => {
try_remove_entry(path);
}
Ok(md) if md.is_dir() => {
try_remove_entry(path);
}
Ok(_) => {
try_remove_entry(path);
}
Err(_) => {}
}
}
#[inline]
pub(crate) fn classify_entry_state(link_path: &Path, expected: &Path) -> EntryState {
match std::fs::read_link(link_path) {
Ok(existing) if existing == expected => {
if link_path.exists() {
EntryState::Fresh
} else {
EntryState::Stale
}
}
Ok(_) => EntryState::Stale,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => EntryState::Missing,
Err(_) => EntryState::Stale,
}
}
#[cfg(test)]
mod tests {
use super::is_physical_importer;
#[test]
fn root_is_physical() {
assert!(is_physical_importer("."));
}
#[test]
fn workspace_paths_are_physical() {
assert!(is_physical_importer("packages/dev/core"));
assert!(is_physical_importer("apps/web"));
assert!(is_physical_importer("libs/@scope/name"));
}
#[test]
fn nested_peer_context_paths_are_virtual() {
assert!(!is_physical_importer(
"packages/dev/addons/node_modules/@dev/core"
));
assert!(!is_physical_importer(
"packages/a/node_modules/@s/b/node_modules/@s/c"
));
}
}