use super::*;
use aube_lockfile::dep_path_filename::{
DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH, dep_path_to_filename,
};
use aube_lockfile::{DepType, DirectDep, LockedPackage, LockfileGraph};
use aube_store::Store;
fn setup_store_with_files(dir: &Path) -> (Store, BTreeMap<String, aube_store::PackageIndex>) {
let store = Store::at(dir.join("store/files"));
let mut indices = BTreeMap::new();
let foo_stored = store
.import_bytes(b"module.exports = 'foo';", false)
.unwrap();
let mut foo_index = PackageIndex::default();
foo_index.insert("index.js".to_string(), foo_stored);
let foo_pkg = store
.import_bytes(b"{\"name\":\"foo\",\"version\":\"1.0.0\"}", false)
.unwrap();
foo_index.insert("package.json".to_string(), foo_pkg);
indices.insert("foo@1.0.0".to_string(), foo_index);
let bar_stored = store
.import_bytes(b"module.exports = 'bar';", false)
.unwrap();
let mut bar_index = PackageIndex::default();
bar_index.insert("index.js".to_string(), bar_stored);
indices.insert("bar@2.0.0".to_string(), bar_index);
(store, indices)
}
fn make_graph() -> LockfileGraph {
let mut packages = BTreeMap::new();
let mut foo_deps = BTreeMap::new();
foo_deps.insert("bar".to_string(), "2.0.0".to_string());
packages.insert(
"foo@1.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "1.0.0".to_string(),
integrity: None,
dependencies: foo_deps,
dep_path: "foo@1.0.0".to_string(),
..Default::default()
},
);
packages.insert(
"bar@2.0.0".to_string(),
LockedPackage {
name: "bar".to_string(),
version: "2.0.0".to_string(),
integrity: None,
dependencies: BTreeMap::new(),
dep_path: "bar@2.0.0".to_string(),
..Default::default()
},
);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "foo".to_string(),
dep_path: "foo@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
LockfileGraph {
importers,
packages,
..Default::default()
}
}
#[test]
fn test_detect_strategy() {
let dir = tempfile::tempdir().unwrap();
let strategy = Linker::detect_strategy(dir.path());
match strategy {
LinkStrategy::Copy => {}
LinkStrategy::Reflink => {
panic!("`auto` probe must resolve same-FS to ReflinkAuto, never plain Reflink")
}
#[cfg(target_os = "macos")]
LinkStrategy::ReflinkAuto => {}
#[cfg(target_os = "macos")]
LinkStrategy::Hardlink => panic!("macOS `auto` must resolve same-FS to ReflinkAuto"),
#[cfg(not(target_os = "macos"))]
LinkStrategy::Hardlink => {}
#[cfg(not(target_os = "macos"))]
LinkStrategy::ReflinkAuto => panic!("non-macOS `auto` must resolve same-FS to Hardlink"),
}
}
const ENCODE_FIXTURES: &[&str] = &[
"foo@1.0.0",
"@scope/bar@2.0.0",
"baz@3.0.0(react@18.2.0)",
"@ng/Core@17.0.0",
"@fig/eslint-config-autocomplete@2.0.0(@typescript-eslint+eslint-plugin@7.18.0(@typescript-eslint+parser@7.18.0(eslint@8.57.1))(eslint@8.57.1))(@typescript-eslint+parser@7.18.0(eslint@8.57.1))(@withfig+eslint-plugin-fig-linter@1.4.1)(eslint@8.57.1)(eslint-plugin-compat@4.2.0(eslint@8.57.1))(typescript@5.9.3)",
];
fn linker_for_encode_test(dir: &Path, hashes: Option<GraphHashes>) -> Linker {
let store = Store::at(dir.join("store/files"));
let mut linker = Linker::new(&store, LinkStrategy::Copy);
if let Some(h) = hashes {
linker = linker.with_graph_hashes(h);
}
linker
}
#[test]
fn precomputed_entry_name_and_subdir_match_recompute_unhashed() {
let dir = tempfile::tempdir().unwrap();
let linker = linker_for_encode_test(dir.path(), None);
for dep_path in ENCODE_FIXTURES {
let precomputed_entry = linker.aube_dir_entry_name(dep_path);
let precomputed_subdir = linker.virtual_store_subdir(dep_path);
let recompute = dep_path_to_filename(dep_path, DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH);
assert_eq!(
precomputed_entry, recompute,
"entry name diverged from the bare-dep_path encode for {dep_path}"
);
assert_eq!(
precomputed_subdir, recompute,
"unhashed subdir diverged from the bare-dep_path encode for {dep_path}"
);
assert_eq!(
precomputed_entry, precomputed_subdir,
"entry name and subdir must coincide in the unhashed mode for {dep_path}"
);
}
}
#[test]
fn precomputed_subdir_matches_recompute_with_graph_hashes() {
let dir = tempfile::tempdir().unwrap();
let mut node_hash = std::collections::BTreeMap::new();
for (i, dep_path) in ENCODE_FIXTURES.iter().enumerate() {
node_hash.insert(
(*dep_path).to_string(),
format!("{:016x}{:016x}", 0x0123_4567_89ab_cdefu64, i as u64),
);
}
let hashes = GraphHashes { node_hash };
let linker = linker_for_encode_test(dir.path(), Some(hashes.clone()));
for dep_path in ENCODE_FIXTURES {
let entry = linker.aube_dir_entry_name(dep_path);
let subdir = linker.virtual_store_subdir(dep_path);
let entry_recompute = dep_path_to_filename(dep_path, DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH);
let subdir_recompute = dep_path_to_filename(
&hashes.hashed_dep_path(dep_path),
DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH,
);
assert_eq!(
entry, entry_recompute,
"entry name diverged from the bare-dep_path encode for {dep_path}"
);
assert_eq!(
subdir, subdir_recompute,
"hashed subdir diverged from the hash-folded encode for {dep_path}"
);
assert_ne!(
entry, subdir,
"graph hash should make subdir differ from entry name for {dep_path}"
);
}
}
#[test]
fn test_link_all_handles_self_referential_dep_at_different_version() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let store = Store::at(dir.path().join("store/files"));
let mut indices = BTreeMap::new();
let host_index_js = store.import_bytes(b"/* react_ujs 3.3.0 */", false).unwrap();
let host_pkg_json = store
.import_bytes(b"{\"name\":\"react_ujs\",\"version\":\"3.3.0\"}", false)
.unwrap();
let mut host_index = PackageIndex::default();
host_index.insert("index.js".to_string(), host_index_js);
host_index.insert("package.json".to_string(), host_pkg_json);
indices.insert("react_ujs@3.3.0".to_string(), host_index);
let mut host_deps = BTreeMap::new();
host_deps.insert("react_ujs".to_string(), "^2.7.1".to_string());
let mut packages = BTreeMap::new();
packages.insert(
"react_ujs@3.3.0".to_string(),
LockedPackage {
name: "react_ujs".to_string(),
version: "3.3.0".to_string(),
integrity: None,
dependencies: host_deps,
dep_path: "react_ujs@3.3.0".to_string(),
..Default::default()
},
);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "react_ujs".to_string(),
dep_path: "react_ujs@3.3.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let stats = linker
.link_all(&project_dir, &graph, &indices)
.expect("install must succeed despite self-named dep");
assert_eq!(stats.packages_linked, 1);
let host_index =
project_dir.join("node_modules/.aube/react_ujs@3.3.0/node_modules/react_ujs/index.js");
assert!(host_index.exists(), "host package files must be present");
}
#[test]
fn test_link_all_creates_pnpm_virtual_store() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let graph = make_graph();
let stats = linker.link_all(&project_dir, &graph, &indices).unwrap();
assert!(project_dir.join("node_modules/.aube").exists());
let aube_foo = project_dir.join("node_modules/.aube/foo@1.0.0");
assert!(aube_foo.symlink_metadata().unwrap().is_symlink());
let foo_in_pnpm = project_dir.join("node_modules/.aube/foo@1.0.0/node_modules/foo/index.js");
assert!(foo_in_pnpm.exists());
assert_eq!(
std::fs::read_to_string(&foo_in_pnpm).unwrap(),
"module.exports = 'foo';"
);
let bar_in_pnpm = project_dir.join("node_modules/.aube/bar@2.0.0/node_modules/bar/index.js");
assert!(bar_in_pnpm.exists());
assert_eq!(stats.packages_linked, 2);
assert!(stats.files_linked >= 3); }
#[test]
fn test_link_file_fresh_reports_missing_cas_shard_and_invalidates_cache() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let foo_index = indices.get("foo@1.0.0").unwrap();
store.save_index("foo", "1.0.0", None, foo_index).unwrap();
let cached_path = store.index_dir().join("foo@1.0.0.json");
assert!(
cached_path.exists(),
"test setup: index cache must be written"
);
let pkgjson_store_path = foo_index.get("package.json").unwrap().store_path.clone();
std::fs::remove_file(&pkgjson_store_path).unwrap();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let graph = make_graph();
let err = linker
.link_all(&project_dir, &graph, &indices)
.expect_err("link must fail when a referenced CAS shard is gone");
assert!(
matches!(&err, Error::MissingStoreFile { rel_path, .. } if rel_path == "package.json"),
"expected MissingStoreFile {{ rel_path: \"package.json\" }}, got {err:?}"
);
assert!(
!cached_path.exists(),
"stale index cache must be invalidated on MissingStoreFile"
);
}
#[test]
#[cfg(unix)]
fn test_link_file_fresh_hardlink_short_circuits_when_source_missing() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("store/files"));
let stored = store.import_bytes(b"hello", false).unwrap();
let store_path = stored.store_path.clone();
std::fs::remove_file(&store_path).unwrap();
let dst_dir = dir.path().join("dst");
std::fs::create_dir_all(&dst_dir).unwrap();
let dst = dst_dir.join("hello.txt");
let linker = Linker::new_with_gvs(&store, LinkStrategy::Hardlink, true);
let err = linker
.link_file_fresh(&stored, "hello.txt", &dst)
.expect_err("source missing must fail");
assert!(
matches!(
&err,
Error::MissingStoreFile { store_path: p, rel_path } if p == &store_path && rel_path == "hello.txt"
),
"expected MissingStoreFile from Hardlink branch, got {err:?}"
);
}
#[cfg(unix)]
struct ForcedReflinkFailure {
_guard: std::sync::MutexGuard<'static, ()>,
}
#[cfg(unix)]
impl ForcedReflinkFailure {
fn engage() -> Self {
use std::sync::atomic::Ordering;
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let guard = LOCK.lock().unwrap_or_else(|e| e.into_inner());
crate::materialize::FORCE_REFLINK_FAILURE.store(true, Ordering::Relaxed);
Self { _guard: guard }
}
}
#[cfg(unix)]
impl Drop for ForcedReflinkFailure {
fn drop(&mut self) {
use std::sync::atomic::Ordering;
crate::materialize::FORCE_REFLINK_FAILURE.store(false, Ordering::Relaxed);
}
}
#[cfg(unix)]
fn realized_inode_matches_source_on_reflink_failure(strategy: LinkStrategy) -> bool {
use std::os::unix::fs::MetadataExt;
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("store/files"));
let content = vec![b'x'; 32 * 1024];
let stored = store.import_bytes(&content, false).unwrap();
let store_path = stored.store_path.clone();
let dst_dir = dir.path().join("dst");
std::fs::create_dir_all(&dst_dir).unwrap();
let dst = dst_dir.join("payload.bin");
let linker = Linker::new_with_gvs(&store, strategy, true);
let result = {
let _forced = ForcedReflinkFailure::engage();
linker.link_file_fresh(&stored, "payload.bin", &dst)
};
result.expect("a reflink strategy must still materialize the file via its fallback");
assert_eq!(std::fs::read(&dst).unwrap(), content);
std::fs::metadata(&store_path).unwrap().ino() == std::fs::metadata(&dst).unwrap().ino()
}
#[test]
#[cfg(unix)]
fn test_reflink_auto_falls_back_to_hardlink_not_copy() {
assert!(
realized_inode_matches_source_on_reflink_failure(LinkStrategy::ReflinkAuto),
"ReflinkAuto must fall back to a hardlink (same inode), not a copy, on reflink failure"
);
}
#[test]
#[cfg(unix)]
fn test_explicit_reflink_falls_back_to_copy_not_hardlink() {
assert!(
!realized_inode_matches_source_on_reflink_failure(LinkStrategy::Reflink),
"explicit Reflink must fall back to a copy (distinct inode), not a hardlink"
);
}
#[test]
fn test_link_all_creates_top_level_entries() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new(&store, LinkStrategy::Copy);
let graph = make_graph();
let stats = linker.link_all(&project_dir, &graph, &indices).unwrap();
let foo_top = project_dir.join("node_modules/foo/index.js");
assert!(foo_top.exists());
assert_eq!(
std::fs::read_to_string(&foo_top).unwrap(),
"module.exports = 'foo';"
);
let bar_top = project_dir.join("node_modules/bar/index.js");
assert!(!bar_top.exists());
assert_eq!(stats.top_level_linked, 1);
}
#[test]
fn test_link_all_transitive_symlinks() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new(&store, LinkStrategy::Copy);
let graph = make_graph();
linker.link_all(&project_dir, &graph, &indices).unwrap();
let bar_symlink = project_dir.join("node_modules/.aube/foo@1.0.0/node_modules/bar");
assert!(bar_symlink.symlink_metadata().unwrap().is_symlink());
}
#[test]
fn test_link_all_cleans_existing_node_modules() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
let nm = project_dir.join("node_modules");
std::fs::create_dir_all(&nm).unwrap();
std::fs::write(nm.join("stale-file.txt"), "old").unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new(&store, LinkStrategy::Copy);
let graph = make_graph();
linker.link_all(&project_dir, &graph, &indices).unwrap();
assert!(!nm.join("stale-file.txt").exists());
assert!(nm.join(".aube").exists());
}
#[test]
fn test_link_all_nested_node_modules_for_direct_deps() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new(&store, LinkStrategy::Copy);
let graph = make_graph();
linker.link_all(&project_dir, &graph, &indices).unwrap();
let foo_link = project_dir.join("node_modules/foo");
assert!(foo_link.symlink_metadata().unwrap().is_symlink());
let bar_sibling = project_dir.join("node_modules/.aube/foo@1.0.0/node_modules/bar");
assert!(bar_sibling.symlink_metadata().unwrap().is_symlink());
}
#[test]
fn test_global_virtual_store_is_populated() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let virtual_store = store.virtual_store_dir();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let graph = make_graph();
linker.link_all(&project_dir, &graph, &indices).unwrap();
let foo_global = virtual_store.join("foo@1.0.0/node_modules/foo/index.js");
assert!(foo_global.exists());
assert_eq!(
std::fs::read_to_string(&foo_global).unwrap(),
"module.exports = 'foo';"
);
let bar_global = virtual_store.join("bar@2.0.0/node_modules/bar/index.js");
assert!(bar_global.exists());
}
#[test]
fn test_global_virtual_store_gets_hidden_hoist() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let virtual_store = store.virtual_store_dir();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let mut graph = make_graph();
graph
.packages
.get_mut("foo@1.0.0")
.unwrap()
.dependencies
.clear();
linker.link_all(&project_dir, &graph, &indices).unwrap();
let project_hidden = project_dir.join("node_modules/.aube/node_modules/bar");
assert!(project_hidden.symlink_metadata().unwrap().is_symlink());
let global_hidden = virtual_store.join("node_modules/bar");
assert!(global_hidden.symlink_metadata().unwrap().is_symlink());
let from_real_store = virtual_store.join("foo@1.0.0/node_modules/bar/index.js");
assert!(
!from_real_store.exists(),
"bar is not a declared sibling of foo in this fixture"
);
let fallback = virtual_store.join("node_modules/bar/index.js");
assert_eq!(
std::fs::read_to_string(fallback).unwrap(),
"module.exports = 'bar';"
);
}
#[test]
fn test_global_virtual_store_hidden_hoist_prunes_only_dead_entries() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let virtual_store = store.virtual_store_dir();
let hidden = virtual_store.join("node_modules");
std::fs::create_dir_all(&hidden).unwrap();
let dotfile = hidden.join(".sentinel");
std::fs::write(&dotfile, "shared").unwrap();
let stale = hidden.join("stale");
std::fs::write(&stale, "old").unwrap();
let stale_scope = hidden.join("@stale-scope");
std::fs::write(&stale_scope, "old").unwrap();
let external_target = virtual_store.join("external@1.0.0/node_modules/external");
std::fs::create_dir_all(&external_target).unwrap();
let external_link = hidden.join("external");
sys::create_dir_link(
&pathdiff::diff_paths(&external_target, &hidden).unwrap(),
&external_link,
)
.unwrap();
let dead_link = hidden.join("dead");
sys::create_dir_link(
Path::new("../missing@1.0.0/node_modules/missing"),
&dead_link,
)
.unwrap();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
linker
.link_all(&project_dir, &make_graph(), &indices)
.unwrap();
assert_eq!(std::fs::read_to_string(dotfile).unwrap(), "shared");
assert!(!stale.exists());
assert!(stale_scope.symlink_metadata().is_err());
assert!(external_link.symlink_metadata().unwrap().is_symlink());
assert!(dead_link.symlink_metadata().is_err());
assert!(hidden.join("bar").symlink_metadata().unwrap().is_symlink());
}
#[test]
fn test_global_virtual_store_hidden_hoist_disabled_keeps_live_shared_links() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let virtual_store = store.virtual_store_dir();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
linker
.link_all(&project_dir, &make_graph(), &indices)
.unwrap();
let global_hidden = virtual_store.join("node_modules/bar");
assert!(global_hidden.symlink_metadata().unwrap().is_symlink());
Linker::new_with_gvs(&store, LinkStrategy::Copy, true)
.with_hoist(false)
.link_all(&project_dir, &make_graph(), &indices)
.unwrap();
assert!(global_hidden.symlink_metadata().unwrap().is_symlink());
}
#[test]
fn test_second_install_reuses_global_store() {
let dir = tempfile::tempdir().unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let graph = make_graph();
let project1 = dir.path().join("project1");
std::fs::create_dir_all(&project1).unwrap();
let stats1 = linker.link_all(&project1, &graph, &indices).unwrap();
assert_eq!(stats1.packages_linked, 2);
assert_eq!(stats1.packages_cached, 0);
let project2 = dir.path().join("project2");
std::fs::create_dir_all(&project2).unwrap();
let stats2 = linker.link_all(&project2, &graph, &indices).unwrap();
assert_eq!(stats2.packages_linked, 0);
assert_eq!(stats2.packages_cached, 2);
assert_eq!(stats2.files_linked, 0);
let foo1 = project1.join("node_modules/foo/index.js");
let foo2 = project2.join("node_modules/foo/index.js");
assert!(foo1.exists());
assert!(foo2.exists());
assert_eq!(
std::fs::read_to_string(&foo1).unwrap(),
std::fs::read_to_string(&foo2).unwrap()
);
}
#[test]
fn gvs_shareable_source_dep_without_index_errors_loudly() {
use aube_lockfile::{GitSource, LocalSource};
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("store/files"));
let git = LocalSource::Git(GitSource {
url: "https://github.com/request/request.git".to_string(),
committish: None,
resolved: "0123456789abcdef0123456789abcdef01234567".to_string(),
integrity: None,
subpath: None,
});
let dep_path = git.dep_path("request");
let mut packages = BTreeMap::new();
packages.insert(
dep_path.clone(),
LockedPackage {
name: "request".to_string(),
version: "2.88.0".to_string(),
integrity: None,
dependencies: BTreeMap::new(),
dep_path: dep_path.clone(),
local_source: Some(git),
..Default::default()
},
);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "request".to_string(),
dep_path: dep_path.clone(),
dep_type: DepType::Production,
specifier: None,
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let indices: BTreeMap<String, aube_store::PackageIndex> = BTreeMap::new();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let err = linker
.link_all(&project_dir, &graph, &indices)
.expect_err("a shareable source dep with no index must error, not dangle");
assert!(
matches!(err, Error::MissingPackageIndex(ref dp) if dp == &dep_path),
"expected MissingPackageIndex({dep_path}), got: {err:?}"
);
}
#[test]
fn test_link_all_repoints_symlink_after_version_bump() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let store = Store::at(dir.path().join("store/files"));
let mut indices_v1 = BTreeMap::new();
let foo_v1 = store
.import_bytes(b"module.exports = 'foo@1';", false)
.unwrap();
let mut foo_v1_index = PackageIndex::default();
foo_v1_index.insert("index.js".to_string(), foo_v1);
indices_v1.insert("foo@1.0.0".to_string(), foo_v1_index);
let mut graph_v1 = LockfileGraph::default();
graph_v1.packages.insert(
"foo@1.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "1.0.0".to_string(),
dep_path: "foo@1.0.0".to_string(),
..Default::default()
},
);
graph_v1.importers.insert(
".".to_string(),
vec![DirectDep {
name: "foo".to_string(),
dep_path: "foo@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
let linker = Linker::new(&store, LinkStrategy::Copy);
linker
.link_all(&project_dir, &graph_v1, &indices_v1)
.unwrap();
let foo_link = project_dir.join("node_modules/foo");
assert!(foo_link.symlink_metadata().unwrap().is_symlink());
assert_eq!(
std::fs::read_to_string(foo_link.join("index.js")).unwrap(),
"module.exports = 'foo@1';"
);
let mut indices_v2 = BTreeMap::new();
let foo_v2 = store
.import_bytes(b"module.exports = 'foo@2';", false)
.unwrap();
let mut foo_v2_index = PackageIndex::default();
foo_v2_index.insert("index.js".to_string(), foo_v2);
indices_v2.insert("foo@2.0.0".to_string(), foo_v2_index);
let mut graph_v2 = LockfileGraph::default();
graph_v2.packages.insert(
"foo@2.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "2.0.0".to_string(),
dep_path: "foo@2.0.0".to_string(),
..Default::default()
},
);
graph_v2.importers.insert(
".".to_string(),
vec![DirectDep {
name: "foo".to_string(),
dep_path: "foo@2.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
linker
.link_all(&project_dir, &graph_v2, &indices_v2)
.unwrap();
assert_eq!(
std::fs::read_to_string(project_dir.join("node_modules/foo/index.js")).unwrap(),
"module.exports = 'foo@2';"
);
}
#[test]
fn test_shamefully_hoist_repoints_after_transitive_version_bump() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let store = Store::at(dir.path().join("store/files"));
let foo_v1 = store
.import_bytes(b"module.exports = 'foo@1';", false)
.unwrap();
let mut foo_v1_idx = PackageIndex::default();
foo_v1_idx.insert("index.js".to_string(), foo_v1);
let bar_v1 = store
.import_bytes(b"module.exports = 'bar@1';", false)
.unwrap();
let mut bar_v1_idx = PackageIndex::default();
bar_v1_idx.insert("index.js".to_string(), bar_v1);
let mut indices_v1 = BTreeMap::new();
indices_v1.insert("foo@1.0.0".to_string(), foo_v1_idx);
indices_v1.insert("bar@1.0.0".to_string(), bar_v1_idx);
let mut graph_v1 = LockfileGraph::default();
let mut bar_deps_v1 = BTreeMap::new();
bar_deps_v1.insert("foo".to_string(), "1.0.0".to_string());
graph_v1.packages.insert(
"bar@1.0.0".to_string(),
LockedPackage {
name: "bar".to_string(),
version: "1.0.0".to_string(),
dep_path: "bar@1.0.0".to_string(),
dependencies: bar_deps_v1,
..Default::default()
},
);
graph_v1.packages.insert(
"foo@1.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "1.0.0".to_string(),
dep_path: "foo@1.0.0".to_string(),
..Default::default()
},
);
graph_v1.importers.insert(
".".to_string(),
vec![DirectDep {
name: "bar".to_string(),
dep_path: "bar@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
let linker = Linker::new(&store, LinkStrategy::Copy).with_shamefully_hoist(true);
linker
.link_all(&project_dir, &graph_v1, &indices_v1)
.unwrap();
assert_eq!(
std::fs::read_to_string(project_dir.join("node_modules/foo/index.js")).unwrap(),
"module.exports = 'foo@1';",
"install 1 should hoist foo@1.0.0"
);
let foo_v2 = store
.import_bytes(b"module.exports = 'foo@2';", false)
.unwrap();
let mut foo_v2_idx = PackageIndex::default();
foo_v2_idx.insert("index.js".to_string(), foo_v2);
let mut indices_v2 = BTreeMap::new();
let bar_v1_for_v2 = store
.import_bytes(b"module.exports = 'bar@1';", false)
.unwrap();
let mut bar_v1_idx_v2 = PackageIndex::default();
bar_v1_idx_v2.insert("index.js".to_string(), bar_v1_for_v2);
indices_v2.insert("bar@1.0.0".to_string(), bar_v1_idx_v2);
indices_v2.insert("foo@2.0.0".to_string(), foo_v2_idx);
let mut graph_v2 = LockfileGraph::default();
let mut bar_deps_v2 = BTreeMap::new();
bar_deps_v2.insert("foo".to_string(), "2.0.0".to_string());
graph_v2.packages.insert(
"bar@1.0.0".to_string(),
LockedPackage {
name: "bar".to_string(),
version: "1.0.0".to_string(),
dep_path: "bar@1.0.0".to_string(),
dependencies: bar_deps_v2,
..Default::default()
},
);
graph_v2.packages.insert(
"foo@2.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "2.0.0".to_string(),
dep_path: "foo@2.0.0".to_string(),
..Default::default()
},
);
graph_v2.importers.insert(
".".to_string(),
vec![DirectDep {
name: "bar".to_string(),
dep_path: "bar@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
linker
.link_all(&project_dir, &graph_v2, &indices_v2)
.unwrap();
assert_eq!(
std::fs::read_to_string(project_dir.join("node_modules/foo/index.js")).unwrap(),
"module.exports = 'foo@2';",
"install 2 should repoint the hoisted symlink to foo@2.0.0"
);
}
#[test]
fn validate_index_key_accepts_normal_keys() {
validate_index_key("index.js").unwrap();
validate_index_key("lib/sub/a.js").unwrap();
validate_index_key("package.json").unwrap();
validate_index_key("a/b/c/d/e/f.js").unwrap();
}
#[cfg(not(windows))]
#[test]
fn validate_index_key_accepts_posix_colon_filename() {
validate_index_key("dist/__mocks__/package-json:version.d.ts").unwrap();
}
#[test]
fn validate_index_key_rejects_empty() {
assert!(matches!(
validate_index_key(""),
Err(Error::UnsafeIndexKey(_))
));
}
#[test]
fn validate_index_key_rejects_leading_slash() {
assert!(matches!(
validate_index_key("/etc/passwd"),
Err(Error::UnsafeIndexKey(_))
));
assert!(matches!(
validate_index_key("\\evil"),
Err(Error::UnsafeIndexKey(_))
));
}
#[test]
fn validate_index_key_rejects_parent_dir() {
assert!(matches!(
validate_index_key("../../etc/passwd"),
Err(Error::UnsafeIndexKey(_))
));
assert!(matches!(
validate_index_key("lib/../../../etc"),
Err(Error::UnsafeIndexKey(_))
));
}
#[test]
fn validate_index_key_rejects_nul_and_backslash() {
assert!(matches!(
validate_index_key("lib\0evil"),
Err(Error::UnsafeIndexKey(_))
));
assert!(matches!(
validate_index_key("lib\\..\\etc"),
Err(Error::UnsafeIndexKey(_))
));
}
#[cfg(windows)]
#[test]
fn validate_index_key_rejects_windows_drive() {
assert!(matches!(
validate_index_key("C:Windows"),
Err(Error::UnsafeIndexKey(_))
));
}