use crate::state;
use miette::miette;
use std::path::{Path, PathBuf};
pub(super) fn resolve_global_virtual_store_override(
settings_ctx: &aube_settings::ResolveCtx<'_>,
manifests: &[(String, aube_manifest::PackageJson)],
env_snapshot: &[(String, String)],
) -> Option<bool> {
let explicit = aube_settings::resolved::enable_global_virtual_store(settings_ctx);
explicit.or_else(|| {
let triggers =
aube_settings::resolved::disable_global_virtual_store_for_packages(settings_ctx);
let triggered_by = super::settings::find_gvs_incompatible_trigger(manifests, &triggers);
let ci_mode = env_snapshot.iter().any(|(k, _)| k == "CI");
let virtual_store_only_setting = aube_settings::resolved::virtual_store_only(settings_ctx);
if let Some(name) = triggered_by
&& !ci_mode
&& !virtual_store_only_setting
{
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_GVS_INCOMPATIBLE,
"`{name}` isn't compatible with aube's global virtual store — \
installing per-project instead. Install still succeeds; repeat \
installs of this project just won't share materialized packages \
across projects. Fixing this requires an upstream change in \
`{name}` itself (please file it with that project, not aube). \
To silence this warning, run `aube config set \
enableGlobalVirtualStore false --location project` — or set \
`disableGlobalVirtualStoreForPackages=[]` to opt out of this \
auto-detection entirely. \
Details: https://aube.jdx.dev/package-manager/global-virtual-store"
);
Some(false)
} else {
None
}
})
}
pub(super) fn planned_global_virtual_store(
use_global_virtual_store_override: Option<bool>,
env_snapshot: &[(String, String)],
) -> bool {
use_global_virtual_store_override
.unwrap_or_else(|| !env_snapshot.iter().any(|(k, _)| k == "CI"))
}
pub(super) fn write_modules_metadata(
workspace_root: &Path,
graph: &aube_lockfile::LockfileGraph,
modules_dir_name: &str,
virtual_store_dir: &Path,
) -> std::io::Result<()> {
let virtual_store_dir = virtual_store_dir.to_string_lossy().into_owned();
for importer_path in graph
.importers
.keys()
.filter(|path| aube_linker::is_physical_importer(path))
{
let project_dir =
super::workspace::importer_project_dir(workspace_root, importer_path.as_str());
let metadata_path = project_dir.join(modules_dir_name).join(".modules.yaml");
let bytes = match std::fs::read(&metadata_path) {
Ok(bytes) => upsert_virtual_store_dir(&bytes, &virtual_store_dir)?,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
let mut metadata = serde_json::Map::new();
metadata.insert(
"virtualStoreDir".to_string(),
serde_json::Value::String(virtual_store_dir.clone()),
);
serde_json::to_vec_pretty(&metadata)?
}
Err(err) => return Err(err),
};
aube_util::fs_atomic::atomic_write(&metadata_path, &bytes)?;
}
Ok(())
}
fn upsert_virtual_store_dir(existing: &[u8], virtual_store_dir: &str) -> std::io::Result<Vec<u8>> {
if let Ok(mut metadata) =
serde_json::from_slice::<serde_json::Map<String, serde_json::Value>>(existing)
{
metadata.insert(
"virtualStoreDir".to_string(),
serde_json::Value::String(virtual_store_dir.to_string()),
);
return serde_json::to_vec_pretty(&metadata).map_err(std::io::Error::other);
}
let source = std::str::from_utf8(existing)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
let mut parsed: yaml_serde::Value = yaml_serde::from_str(source)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
if !parsed.is_mapping() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"node_modules/.modules.yaml must contain a mapping",
));
}
let quoted = serde_json::to_string(virtual_store_dir).map_err(std::io::Error::other)?;
let flow_mapping = source
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.find_map(|line| {
if line.starts_with('%') || line == "---" || line == "..." {
return None;
}
let content = line
.strip_prefix("---")
.map(str::trim_start)
.unwrap_or(line);
(!content.is_empty()).then(|| content.starts_with('{'))
})
.unwrap_or(false);
let block_scalar = source.lines().any(|line| {
let line = line.trim_end();
!line.chars().next().is_some_and(char::is_whitespace)
&& line.split_once(':').is_some_and(|(key, value)| {
key.trim_matches([' ', '"', '\'']) == "virtualStoreDir"
&& matches!(value.trim_start().chars().next(), Some('|' | '>'))
})
});
let explicit_document_end = source.lines().any(|line| line.trim() == "...");
if flow_mapping || block_scalar || explicit_document_end {
let Some(mapping) = parsed.as_mapping_mut() else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"node_modules/.modules.yaml must contain a mapping",
));
};
mapping.insert(
yaml_serde::Value::String("virtualStoreDir".to_string()),
yaml_serde::Value::String(virtual_store_dir.to_string()),
);
return yaml_serde::to_string(&parsed)
.map(String::into_bytes)
.map_err(std::io::Error::other);
}
let replacement = format!("virtualStoreDir: {quoted}");
let mut output = String::with_capacity(source.len() + replacement.len() + 1);
let mut replaced = false;
for segment in source.split_inclusive('\n') {
let line = segment.strip_suffix('\n').unwrap_or(segment);
let line = line.strip_suffix('\r').unwrap_or(line);
let is_top_level_key = !line.chars().next().is_some_and(char::is_whitespace)
&& line
.split_once(':')
.is_some_and(|(key, _)| key.trim_matches([' ', '"', '\'']) == "virtualStoreDir");
if is_top_level_key {
output.push_str(&replacement);
if segment.ends_with("\r\n") {
output.push_str("\r\n");
} else if segment.ends_with('\n') {
output.push('\n');
}
replaced = true;
} else {
output.push_str(segment);
}
}
if !replaced {
if !output.is_empty() && !output.ends_with('\n') {
output.push('\n');
}
output.push_str(&replacement);
output.push('\n');
}
Ok(output.into_bytes())
}
fn metadata_virtual_store_dir(bytes: &[u8]) -> Option<String> {
if let Ok(metadata) =
serde_json::from_slice::<serde_json::Map<String, serde_json::Value>>(bytes)
{
return metadata
.get("virtualStoreDir")
.and_then(serde_json::Value::as_str)
.map(str::to_string);
}
let source = std::str::from_utf8(bytes).ok()?;
let metadata: yaml_serde::Value = yaml_serde::from_str(source).ok()?;
metadata
.as_mapping()?
.get(yaml_serde::Value::String("virtualStoreDir".to_string()))?
.as_str()
.map(str::to_string)
}
pub(super) fn modules_metadata_is_current<'a>(
workspace_root: &Path,
importer_paths: impl IntoIterator<Item = &'a str>,
modules_dir_name: &str,
expected_virtual_store_dir: Option<&Path>,
) -> bool {
importer_paths
.into_iter()
.filter(|path| aube_linker::is_physical_importer(path))
.all(|importer_path| {
let project_dir = super::workspace::importer_project_dir(workspace_root, importer_path);
let metadata_path = project_dir.join(modules_dir_name).join(".modules.yaml");
let Some(actual) = std::fs::read(metadata_path)
.ok()
.and_then(|bytes| metadata_virtual_store_dir(&bytes))
else {
tracing::debug!(
"install warm path skipped: {} has no virtualStoreDir",
project_dir
.join(modules_dir_name)
.join(".modules.yaml")
.display()
);
return false;
};
let matches = expected_virtual_store_dir
.is_none_or(|expected| actual.as_str() == expected.to_string_lossy().as_ref());
if !matches {
tracing::debug!(
"install warm path skipped: {} has virtualStoreDir={actual}, expected {}",
project_dir
.join(modules_dir_name)
.join(".modules.yaml")
.display(),
expected_virtual_store_dir
.map_or_else(|| "<any>".into(), |path| path.to_string_lossy())
);
}
matches
})
}
pub(crate) fn detect_existing_global_virtual_store(
workspace_root: &Path,
aube_dir: &Path,
modules_dir_name: &str,
global_virtual_store: &Path,
) -> Option<bool> {
let mut existing_gvs = super::settings::detect_aube_dir_gvs_mode(aube_dir)?;
if !existing_gvs {
let metadata_path = workspace_root.join(modules_dir_name).join(".modules.yaml");
existing_gvs = std::fs::read(metadata_path)
.ok()
.and_then(|bytes| metadata_virtual_store_dir(&bytes))
.map(PathBuf::from)
.map(|path| {
if path.is_absolute() {
path
} else {
workspace_root.join(modules_dir_name).join(path)
}
})
.is_some_and(|path| {
let actual = std::fs::canonicalize(&path).unwrap_or(path);
let expected = std::fs::canonicalize(global_virtual_store)
.unwrap_or_else(|_| global_virtual_store.to_path_buf());
actual == expected
});
}
Some(existing_gvs)
}
fn legacy_vite_dep_paths(
graph: &aube_lockfile::LockfileGraph,
) -> std::collections::BTreeSet<String> {
graph
.packages
.iter()
.filter(|(_, pkg)| {
pkg.registry_name() == "vite" && vite_needs_modules_metadata_backport(&pkg.version)
})
.map(|(dep_path, _)| dep_path.clone())
.collect()
}
pub(super) fn legacy_vite_project_local_closure(
graph: &aube_lockfile::LockfileGraph,
) -> std::collections::BTreeSet<String> {
let mut selected = legacy_vite_dep_paths(graph);
if selected.is_empty() {
return selected;
}
loop {
let mut added = false;
for (parent_path, parent) in &graph.packages {
if selected.contains(parent_path) {
continue;
}
let reaches_selected = parent.dependencies.iter().any(|(name, tail)| {
aube_lockfile::resolve_dep_edge(name, tail, |key| graph.packages.contains_key(key))
.is_some_and(|child| selected.contains(&child))
});
if reaches_selected {
selected.insert(parent_path.clone());
added = true;
}
}
if !added {
break;
}
}
selected
}
fn vite_needs_modules_metadata_backport(version: &str) -> bool {
let core = version.split(['-', '+']).next().unwrap_or(version);
let mut parts = core.split('.');
let Some(major) = parts.next().and_then(|part| part.parse::<u32>().ok()) else {
return false;
};
if major != 8 {
return major < 8;
}
parts
.next()
.and_then(|part| part.parse::<u32>().ok())
.unwrap_or(0)
< 1
}
const VITE_COMPAT_PREPEND: &str = "import{readFileSync as __aubeRfs}from\"node:fs\";\
import{join as __aubeJoin,isAbsolute as __aubeIsAbs,resolve as __aubeResolve}from\"node:path\";\n";
const VITE_COMPAT_ANCHORS: &[&str] = &[
"let allowDirs = server.fs.allow;",
"let allowDirs = server.fs?.allow;",
"let allowDirs = (_a = server.fs) === null || _a === void 0 ? void 0 : _a.allow;",
];
const VITE_COMPAT_MARKER: &str = "__aubeRfs(__aubeJoin";
pub(super) fn patch_legacy_vite_copies(
aube_dir: &Path,
graph: &aube_lockfile::LockfileGraph,
virtual_store_dir_max_length: usize,
global_virtual_store: &Path,
modules_dir_name: &str,
) -> std::io::Result<usize> {
let global_virtual_store = std::fs::canonicalize(global_virtual_store)
.unwrap_or_else(|_| global_virtual_store.to_path_buf());
let mut patched = 0;
for dep_path in legacy_vite_dep_paths(graph) {
let Some(pkg) = graph.packages.get(&dep_path) else {
continue;
};
let entry = aube_lockfile::dep_path_filename::dep_path_to_filename(
&dep_path,
virtual_store_dir_max_length,
);
let vite_dir = aube_dir.join(entry).join("node_modules").join(&pkg.name);
let real_vite_dir = match std::fs::canonicalize(&vite_dir) {
Ok(path) => path,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(error),
};
if real_vite_dir.starts_with(&global_virtual_store) {
return Err(std::io::Error::other(format!(
"refusing to patch shared Vite package at {}",
real_vite_dir.display()
)));
}
let dist_node = real_vite_dir.join("dist").join("node");
if !dist_node.is_dir() {
continue;
}
let mut files = Vec::new();
collect_vite_js(&dist_node, &mut files, 0)?;
for file in files {
patched += usize::from(patch_vite_file(&file, modules_dir_name)?);
}
}
Ok(patched)
}
pub(super) fn legacy_vite_patches_are_current(aube_dir: &Path) -> bool {
let Ok(entries) = std::fs::read_dir(aube_dir) else {
return false;
};
let mut inspected = std::collections::BTreeSet::new();
for entry in entries.flatten() {
let modules_dir = entry.path().join("node_modules");
let Ok(packages) = std::fs::read_dir(modules_dir) else {
continue;
};
for package in packages.flatten() {
let package_dir = package.path();
let Ok(bytes) = std::fs::read(package_dir.join("package.json")) else {
continue;
};
let Ok(manifest) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
return false;
};
if manifest.get("name").and_then(serde_json::Value::as_str) != Some("vite") {
continue;
}
let Some(version) = manifest.get("version").and_then(serde_json::Value::as_str) else {
return false;
};
if !vite_needs_modules_metadata_backport(version) {
continue;
}
let real_package_dir =
std::fs::canonicalize(&package_dir).unwrap_or_else(|_| package_dir.clone());
if !inspected.insert(real_package_dir.clone()) {
continue;
}
let mut files = Vec::new();
if collect_vite_js(&real_package_dir.join("dist").join("node"), &mut files, 0).is_err()
{
return false;
}
for file in files {
let Ok(source) = std::fs::read_to_string(file) else {
return false;
};
if !source.contains(VITE_COMPAT_MARKER)
&& VITE_COMPAT_ANCHORS
.iter()
.any(|anchor| source.contains(anchor))
{
return false;
}
}
}
}
true
}
fn collect_vite_js(
dir: &Path,
out: &mut Vec<std::path::PathBuf>,
depth: usize,
) -> std::io::Result<()> {
if depth > 8 {
return Ok(());
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;
if file_type.is_dir() {
collect_vite_js(&path, out, depth + 1)?;
} else if file_type.is_file() && path.extension().is_some_and(|ext| ext == "js") {
out.push(path);
}
}
Ok(())
}
fn patch_vite_file(file: &Path, modules_dir_name: &str) -> std::io::Result<bool> {
let source = std::fs::read_to_string(file)?;
if source.contains(VITE_COMPAT_MARKER) {
return Ok(false);
}
let Some(anchor) = VITE_COMPAT_ANCHORS
.iter()
.find(|anchor| source.contains(**anchor))
else {
return Ok(false);
};
let modules_dir = serde_json::to_string(modules_dir_name).map_err(std::io::Error::other)?;
let insert = format!(
";const __awr=searchForWorkspaceRoot(root);\
if(!allowDirs)allowDirs=[__awr];\
try{{const __ac=__aubeRfs(__aubeJoin(__awr,{modules_dir},\".modules.yaml\"),\"utf-8\");\
let __av;try{{__av=JSON.parse(__ac).virtualStoreDir;}}catch{{const __am=__ac.match(/^virtualStoreDir:\\s*(.+?)\\s*(?:#.*)?$/m);\
if(__am)try{{__av=JSON.parse(__am[1]);}}catch{{__av=__am[1];}}}}\
if(__av){{if(__aubeIsAbs(__av))allowDirs.push(__av);\
else if(__av.startsWith(\"..\"))allowDirs.push(__aubeResolve(__aubeJoin(__awr,{modules_dir}),__av));}}}}catch{{void 0;}}"
);
let patched = source.replacen(anchor, &format!("{anchor}{insert}"), 1);
let patched = format!("{VITE_COMPAT_PREPEND}{patched}");
aube_util::fs_atomic::atomic_write(file, patched.as_bytes())?;
Ok(true)
}
pub(super) fn reset_on_mode_change(
cwd: &Path,
aube_dir: &Path,
modules_dir_name: &str,
planned_gvs: bool,
settings_ctx: &aube_settings::ResolveCtx<'_>,
) -> miette::Result<()> {
let global_virtual_store =
crate::commands::global_virtual_store_dir_with_ctx(cwd, settings_ctx);
let Some(existing_gvs) = detect_existing_global_virtual_store(
cwd,
aube_dir,
modules_dir_name,
&global_virtual_store,
) else {
return Ok(());
};
if existing_gvs == planned_gvs {
return Ok(());
}
let from = if existing_gvs { "enabled" } else { "disabled" };
let to = if planned_gvs { "enabled" } else { "disabled" };
let modules_dir_path = cwd.join(modules_dir_name);
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_GVS_MODE_CHANGED,
"global virtual store {from} → {to}; removing {} and reinstalling from scratch",
modules_dir_path.display()
);
remove_dir_all_if_exists(&modules_dir_path).map_err(|e| {
miette!(
"global virtual store transition: failed to remove {}: {e}",
modules_dir_path.display()
)
})?;
if !aube_dir.starts_with(&modules_dir_path) {
remove_dir_all_if_exists(aube_dir).map_err(|e| {
miette!(
"global virtual store transition: failed to remove {}: {e}",
aube_dir.display()
)
})?;
}
state::remove_state(cwd).map_err(|e| {
miette!("global virtual store transition: failed to remove install state: {e}")
})
}
fn remove_dir_all_if_exists(path: &Path) -> std::io::Result<()> {
match std::fs::remove_dir_all(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use aube_lockfile::{DepType, DirectDep, LockedPackage};
#[test]
fn writes_vite_metadata_for_each_importer_and_preserves_unknown_keys() {
let tmp = tempfile::tempdir().expect("tempdir should be created");
let root = tmp.path();
std::fs::create_dir_all(root.join("node_modules"))
.expect("root node_modules should be created");
std::fs::create_dir_all(root.join("packages/app/node_modules"))
.expect("member node_modules should be created");
std::fs::write(
root.join("node_modules/.modules.yaml"),
br#"{"packageManager":"pnpm@11.0.0","virtualStoreDir":".pnpm"}"#,
)
.expect("existing metadata should be written");
std::fs::write(
root.join("packages/app/node_modules/.modules.yaml"),
"layoutVersion: 5\n# preserve this comment\nvirtualStoreDir: .pnpm\nhoistPattern:\n - '*'\n",
)
.expect("existing pnpm YAML should be written");
let mut graph = aube_lockfile::LockfileGraph::default();
graph.importers.insert(".".to_string(), Default::default());
graph
.importers
.insert("packages/app".to_string(), Default::default());
let synthetic_importer = "packages/app/node_modules/vite@5.4.21";
graph
.importers
.insert(synthetic_importer.to_string(), Default::default());
let store = root.join("shared/virtual-store");
write_modules_metadata(root, &graph, "node_modules", &store)
.expect("metadata should be written");
for project in [root.to_path_buf(), root.join("packages/app")] {
let bytes = std::fs::read(project.join("node_modules/.modules.yaml"))
.expect("metadata should exist");
assert_eq!(
metadata_virtual_store_dir(&bytes).as_deref(),
Some(store.to_string_lossy().as_ref())
);
}
let root_metadata: serde_json::Value = serde_json::from_slice(
&std::fs::read(root.join("node_modules/.modules.yaml"))
.expect("root metadata should exist"),
)
.expect("root metadata should parse");
assert_eq!(root_metadata["packageManager"], "pnpm@11.0.0");
let member_metadata =
std::fs::read_to_string(root.join("packages/app/node_modules/.modules.yaml"))
.expect("member metadata should be readable");
assert!(member_metadata.contains("layoutVersion: 5"));
assert!(member_metadata.contains("# preserve this comment"));
assert!(member_metadata.contains("hoistPattern:\n - '*'"));
assert_eq!(
metadata_virtual_store_dir(member_metadata.as_bytes()).as_deref(),
Some(store.to_string_lossy().as_ref())
);
assert!(
!root
.join(synthetic_importer)
.join("node_modules/.modules.yaml")
.exists(),
"synthetic peer-context importers must not get filesystem metadata"
);
assert!(modules_metadata_is_current(
root,
graph.importers.keys().map(String::as_str),
"node_modules",
Some(&store)
));
std::fs::remove_file(root.join("packages/app/node_modules/.modules.yaml"))
.expect("member metadata should be removed");
assert!(!modules_metadata_is_current(
root,
graph.importers.keys().map(String::as_str),
"node_modules",
Some(&store)
));
}
#[test]
fn rewrites_flow_style_yaml_as_a_valid_mapping() {
for existing in [
"{virtualStoreDir: .pnpm, layoutVersion: 5}\n",
"%YAML 1.2\n---\n{virtualStoreDir: .pnpm, layoutVersion: 5}\n",
] {
let updated = upsert_virtual_store_dir(existing.as_bytes(), "/shared/store")
.expect("metadata should update");
let parsed: yaml_serde::Value = yaml_serde::from_slice(&updated)
.expect("updated metadata should remain valid YAML");
let mapping = parsed
.as_mapping()
.expect("updated metadata should remain a mapping");
assert_eq!(
mapping
.get(yaml_serde::Value::String("virtualStoreDir".to_string()))
.and_then(yaml_serde::Value::as_str),
Some("/shared/store")
);
assert_eq!(
mapping
.get(yaml_serde::Value::String("layoutVersion".to_string()))
.and_then(yaml_serde::Value::as_i64),
Some(5)
);
}
}
#[test]
fn rewrites_block_scalar_yaml_without_leaving_scalar_lines() {
let existing = b"virtualStoreDir: |-\n .pnpm\nlayoutVersion: 5\nhoistPattern:\n - '*'\n";
let updated =
upsert_virtual_store_dir(existing, "/shared/store").expect("metadata should update");
let parsed: yaml_serde::Value =
yaml_serde::from_slice(&updated).expect("updated metadata should remain valid YAML");
let mapping = parsed
.as_mapping()
.expect("updated metadata should remain a mapping");
assert_eq!(
mapping
.get(yaml_serde::Value::String("virtualStoreDir".to_string()))
.and_then(yaml_serde::Value::as_str),
Some("/shared/store")
);
assert_eq!(
mapping
.get(yaml_serde::Value::String("layoutVersion".to_string()))
.and_then(yaml_serde::Value::as_i64),
Some(5)
);
}
#[test]
fn inserts_metadata_before_an_explicit_document_end() {
let existing = b"layoutVersion: 5\nhoistPattern:\n - '*'\n...\n";
let updated =
upsert_virtual_store_dir(existing, "/shared/store").expect("metadata should update");
let parsed: yaml_serde::Value =
yaml_serde::from_slice(&updated).expect("updated metadata should remain valid YAML");
let mapping = parsed
.as_mapping()
.expect("updated metadata should remain a mapping");
assert_eq!(
mapping
.get(yaml_serde::Value::String("virtualStoreDir".to_string()))
.and_then(yaml_serde::Value::as_str),
Some("/shared/store")
);
}
#[test]
fn metadata_disambiguates_gvs_trees_with_only_local_packages() {
let tmp = tempfile::tempdir().expect("tempdir should be created");
let root = tmp.path();
let aube_dir = root.join("node_modules/.aube");
std::fs::create_dir_all(aube_dir.join("vendor-dir@9.9.9"))
.expect("local package should be created");
let shared_store = root.join("shared/virtual-store");
std::fs::write(
root.join("node_modules/.modules.yaml"),
format!(
"virtualStoreDir: {}\n",
serde_json::to_string(&shared_store.to_string_lossy())
.expect("store path should serialize")
),
)
.expect("metadata should be written");
assert_eq!(
detect_existing_global_virtual_store(root, &aube_dir, "node_modules", &shared_store),
Some(true)
);
std::fs::write(
root.join("node_modules/.modules.yaml"),
"virtualStoreDir: .pnpm\n",
)
.expect("pnpm metadata should be written");
assert_eq!(
detect_existing_global_virtual_store(root, &aube_dir, "node_modules", &shared_store),
Some(false),
"pnpm's relative local store must not imply aube GVS mode"
);
}
#[test]
fn legacy_direct_vite_is_selected_but_modern_vite_is_not() {
let mut graph = aube_lockfile::LockfileGraph::default();
graph.importers.insert(
".".to_string(),
vec![
DirectDep {
name: "vite".to_string(),
dep_path: "vite@7.3.6".to_string(),
dep_type: DepType::Dev,
specifier: Some("^7.0.0".to_string()),
},
DirectDep {
name: "vite".to_string(),
dep_path: "vite@8.1.0".to_string(),
dep_type: DepType::Dev,
specifier: Some("^8.1.0".to_string()),
},
],
);
for version in ["7.3.6", "8.1.0"] {
let dep_path = format!("vite@{version}");
graph.packages.insert(
dep_path.clone(),
LockedPackage {
name: "vite".to_string(),
version: version.to_string(),
dep_path,
..Default::default()
},
);
}
assert_eq!(
legacy_vite_dep_paths(&graph),
["vite@7.3.6".to_string()].into_iter().collect()
);
}
#[test]
fn legacy_vite_local_closure_includes_framework_ancestors_only() {
let mut graph = aube_lockfile::LockfileGraph::default();
graph.importers.insert(
".".to_string(),
vec![DirectDep {
name: "vitepress".to_string(),
dep_path: "vitepress@1.6.4".to_string(),
dep_type: DepType::Dev,
specifier: Some("^1.6.0".to_string()),
}],
);
graph.packages.insert(
"vite@5.4.21".to_string(),
LockedPackage {
name: "vite".to_string(),
version: "5.4.21".to_string(),
dep_path: "vite@5.4.21".to_string(),
..Default::default()
},
);
graph.packages.insert(
"vitepress@1.6.4".to_string(),
LockedPackage {
name: "vitepress".to_string(),
version: "1.6.4".to_string(),
dep_path: "vitepress@1.6.4".to_string(),
dependencies: [("vite".to_string(), "5.4.21".to_string())]
.into_iter()
.collect(),
..Default::default()
},
);
graph.packages.insert(
"unrelated@1.0.0".to_string(),
LockedPackage {
name: "unrelated".to_string(),
version: "1.0.0".to_string(),
dep_path: "unrelated@1.0.0".to_string(),
..Default::default()
},
);
assert_eq!(
legacy_vite_project_local_closure(&graph),
["vite@5.4.21".to_string(), "vitepress@1.6.4".to_string()]
.into_iter()
.collect()
);
}
#[test]
fn legacy_vite_patch_is_anchored_idempotent_and_breaks_cas_hardlinks() {
let tmp = tempfile::tempdir().expect("tempdir should be created");
let aube_dir = tmp.path().join("project/node_modules/.aube");
let vite_dir = aube_dir.join("vite@7.3.6/node_modules/vite");
let chunks = vite_dir.join("dist/node/chunks");
let global = tmp.path().join("global-virtual-store");
std::fs::create_dir_all(&chunks).expect("Vite chunks should be created");
std::fs::create_dir_all(&global).expect("global store should be created");
std::fs::write(
vite_dir.join("package.json"),
r#"{"name":"vite","version":"7.3.6"}"#,
)
.expect("Vite manifest should be written");
let mut graph = aube_lockfile::LockfileGraph::default();
graph.importers.insert(
".".to_string(),
vec![DirectDep {
name: "vite".to_string(),
dep_path: "vite@7.3.6".to_string(),
dep_type: DepType::Dev,
specifier: Some("^7.0.0".to_string()),
}],
);
graph.packages.insert(
"vite@7.3.6".to_string(),
LockedPackage {
name: "vite".to_string(),
version: "7.3.6".to_string(),
dep_path: "vite@7.3.6".to_string(),
..Default::default()
},
);
let cas = tmp.path().join("cas.js");
let chunk = chunks.join("config.js");
let source = "let allowDirs = server.fs.allow;\n";
std::fs::write(&cas, source).expect("CAS stand-in should be written");
std::fs::hard_link(&cas, &chunk).expect("installed chunk should hardlink to CAS stand-in");
let patched = patch_legacy_vite_copies(
&aube_dir,
&graph,
aube_lockfile::dep_path_filename::DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH,
&global,
"vendor_modules",
)
.expect("legacy Vite should be patched");
assert_eq!(patched, 1);
let output = std::fs::read_to_string(&chunk).expect("patched chunk should be readable");
assert!(output.starts_with(VITE_COMPAT_PREPEND));
assert!(output.contains(VITE_COMPAT_MARKER));
assert!(output.contains(r#"__aubeJoin(__awr,"vendor_modules",".modules.yaml")"#));
assert!(!output.contains(r#"__aubeJoin(__awr,"node_modules",".modules.yaml")"#));
assert_eq!(
std::fs::read_to_string(&cas).expect("CAS stand-in should be readable"),
source,
"atomic replacement must not mutate the shared hardlink inode"
);
let second = patch_legacy_vite_copies(
&aube_dir,
&graph,
aube_lockfile::dep_path_filename::DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH,
&global,
"vendor_modules",
)
.expect("second patch pass should succeed");
assert_eq!(second, 0);
assert!(legacy_vite_patches_are_current(&aube_dir));
std::fs::write(&chunk, source).expect("unpatched Vite chunk should be restored");
assert!(!legacy_vite_patches_are_current(&aube_dir));
std::fs::write(&chunk, "export const untouched = true;\n")
.expect("anchorless Vite chunk should be written");
assert!(
legacy_vite_patches_are_current(&aube_dir),
"an anchorless bundle is intentionally skipped by the patcher"
);
}
#[test]
fn legacy_vite_patch_matches_vite_two_through_eight_bundles() {
let tmp = tempfile::tempdir().expect("tempdir should be created");
for (index, anchor) in VITE_COMPAT_ANCHORS.iter().enumerate() {
let chunk = tmp.path().join(format!("chunk-{index}.js"));
std::fs::write(&chunk, format!("{anchor}\n"))
.expect("synthetic Vite chunk should be written");
assert!(patch_vite_file(&chunk, "node_modules").expect("matching anchor should patch"));
let output = std::fs::read_to_string(&chunk).expect("patched chunk should be readable");
assert!(output.contains(VITE_COMPAT_MARKER));
}
}
}