use std::{
env,
path::{Path, PathBuf},
};
use super::error::CorsaResult;
use super::materialize_fs::{ensure_dir, prune_dir_entries, remove_path, write_if_changed};
use vize_carton::FxHashSet;
const VIZE_VUE_PACKAGE_ENV: &str = "VIZE_VUE_PACKAGE";
const VIZE_RUNTIME_NODE_MODULES_ENV: &str = "VIZE_RUNTIME_NODE_MODULES";
#[cfg(test)]
const VIZE_TEST_WORKSPACE_NODE_MODULES_ENV: &str = "VIZE_TEST_WORKSPACE_NODE_MODULES";
const VUE_FACADE_PACKAGE_JSON: &str = r#"{
"name": "vue",
"types": "index.d.ts"
}
"#;
const VUE_FACADE_TYPES: &str = r#"export * from "@vue/runtime-dom";
"#;
const VITE_STUB_PACKAGE_JSON: &str = r#"{
"name": "vite",
"types": "client.d.ts"
}
"#;
const VITE_CLIENT_STUB: &str = r#"interface ImportMetaEnv {
readonly [key: string]: string | boolean | undefined;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
export {};
"#;
pub(super) fn materialize_runtime_dependencies(
project_root: &Path,
virtual_root: &Path,
) -> CorsaResult<()> {
let node_modules_dir = virtual_root.join("node_modules");
ensure_dir(&node_modules_dir)?;
materialize_vue_support(project_root, &node_modules_dir)?;
materialize_vite_support(project_root, &node_modules_dir)?;
prune_runtime_node_modules(&node_modules_dir)?;
Ok(())
}
fn materialize_vue_support(project_root: &Path, node_modules_dir: &Path) -> std::io::Result<()> {
let vue_target = node_modules_dir.join("vue");
let vue_namespace_target = node_modules_dir.join("@vue");
if let Some(vue_source) = resolve_vue_package(project_root)
&& symlink_path(&vue_source, &vue_target).is_ok()
{
if let Some(vue_namespace_source) = resolve_vue_namespace_package(project_root, &vue_source)
{
if symlink_path(&vue_namespace_source, &vue_namespace_target).is_err() {
remove_path(&vue_namespace_target)?;
}
} else {
remove_path(&vue_namespace_target)?;
}
return Ok(());
}
if let Some(runtime_dom_source) = resolve_package(project_root, "@vue/runtime-dom") {
write_vue_facade(node_modules_dir)?;
if let Some(vue_namespace_source) =
resolve_adjacent_vue_namespace_package(&runtime_dom_source)
{
if symlink_path(&vue_namespace_source, &vue_namespace_target).is_err() {
remove_path(&vue_namespace_target)?;
}
} else {
remove_path(&vue_namespace_target)?;
}
return Ok(());
}
remove_path(&vue_target)?;
remove_path(&vue_namespace_target)?;
Ok(())
}
fn materialize_vite_support(project_root: &Path, node_modules_dir: &Path) -> std::io::Result<()> {
let vite_target = node_modules_dir.join("vite");
if let Some(vite_source) = resolve_ancestor_package(project_root, "vite")
&& symlink_path(&vite_source, &vite_target).is_ok()
{
return Ok(());
}
write_vite_stub(node_modules_dir)
}
fn resolve_vue_namespace_package(project_root: &Path, vue_source: &Path) -> Option<PathBuf> {
let adjacent = resolve_adjacent_vue_namespace_package(vue_source);
let ancestor = resolve_ancestor_package(project_root, "@vue");
adjacent
.as_ref()
.filter(|path| is_vue_runtime_namespace(path))
.cloned()
.or_else(|| {
ancestor
.as_ref()
.filter(|path| is_vue_runtime_namespace(path))
.cloned()
})
.or(adjacent)
.or(ancestor)
.or_else(|| resolve_package_from_runtime_node_modules("@vue"))
.or_else(|| resolve_test_workspace_package("@vue"))
}
fn resolve_adjacent_vue_namespace_package(vue_source: &Path) -> Option<PathBuf> {
let mut candidates = Vec::new();
if let Some(parent) = vue_source.parent() {
candidates.push(parent.join("@vue"));
}
if let Ok(real_vue_source) = std::fs::canonicalize(vue_source)
&& let Some(parent) = real_vue_source.parent()
{
candidates.push(parent.join("@vue"));
}
candidates
.iter()
.find(|candidate| candidate.exists() && is_vue_runtime_namespace(candidate))
.cloned()
.or_else(|| candidates.into_iter().find(|candidate| candidate.exists()))
}
fn is_vue_runtime_namespace(path: &Path) -> bool {
path.join("runtime-dom").exists() || path.join("runtime-core").exists()
}
fn resolve_vue_package(project_root: &Path) -> Option<PathBuf> {
resolve_ancestor_package(project_root, "vue")
.or_else(|| resolve_explicit_package_env(VIZE_VUE_PACKAGE_ENV))
.or_else(|| resolve_package_from_runtime_node_modules("vue"))
.or_else(|| resolve_test_workspace_package("vue"))
}
fn resolve_package(project_root: &Path, package: &str) -> Option<PathBuf> {
resolve_ancestor_package(project_root, package)
.or_else(|| resolve_package_from_runtime_node_modules(package))
.or_else(|| resolve_test_workspace_package(package))
}
fn resolve_explicit_package_env(name: &str) -> Option<PathBuf> {
env::var_os(name)
.map(PathBuf::from)
.filter(|path| path.exists())
}
fn resolve_package_from_runtime_node_modules(package: &str) -> Option<PathBuf> {
env::var_os(VIZE_RUNTIME_NODE_MODULES_ENV)
.into_iter()
.flat_map(|paths| env::split_paths(&paths).collect::<Vec<_>>())
.map(|node_modules| node_modules.join(package_path(package)))
.find(|candidate| candidate.exists())
}
fn package_path(package: &str) -> PathBuf {
package.split('/').collect()
}
#[cfg(test)]
fn resolve_test_workspace_package(package: &str) -> Option<PathBuf> {
if let Some(override_path) = env::var_os(VIZE_TEST_WORKSPACE_NODE_MODULES_ENV) {
if override_path.as_os_str() == "__none__" {
return None;
}
let candidate = PathBuf::from(override_path).join(package_path(package));
return candidate.exists().then_some(candidate);
}
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)?;
let candidate = workspace_root
.join("node_modules")
.join(package_path(package));
candidate.exists().then_some(candidate)
}
#[cfg(not(test))]
fn resolve_test_workspace_package(_package: &str) -> Option<PathBuf> {
None
}
fn resolve_ancestor_package(project_root: &Path, package: &str) -> Option<PathBuf> {
let mut current = Some(project_root);
while let Some(dir) = current {
let candidate = dir.join("node_modules").join(package_path(package));
if candidate.exists() {
return Some(candidate);
}
current = dir.parent();
}
None
}
fn write_vue_facade(node_modules_dir: &Path) -> std::io::Result<()> {
let vue_dir = node_modules_dir.join("vue");
ensure_stub_dir(&vue_dir)?;
write_if_changed(
&vue_dir.join("package.json"),
VUE_FACADE_PACKAGE_JSON.as_bytes(),
)?;
write_if_changed(&vue_dir.join("index.d.ts"), VUE_FACADE_TYPES.as_bytes())?;
prune_stub_dir(&vue_dir, &["package.json", "index.d.ts"])?;
Ok(())
}
fn write_vite_stub(node_modules_dir: &Path) -> std::io::Result<()> {
let vite_dir = node_modules_dir.join("vite");
ensure_stub_dir(&vite_dir)?;
write_if_changed(
&vite_dir.join("package.json"),
VITE_STUB_PACKAGE_JSON.as_bytes(),
)?;
write_if_changed(&vite_dir.join("client.d.ts"), VITE_CLIENT_STUB.as_bytes())?;
prune_stub_dir(&vite_dir, &["package.json", "client.d.ts"])?;
Ok(())
}
fn ensure_stub_dir(path: &Path) -> std::io::Result<()> {
match std::fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_dir() && !metadata.file_type().is_symlink() => {}
Ok(_) => {
remove_path(path)?;
ensure_dir(path)?;
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => ensure_dir(path)?,
Err(error) => return Err(error),
}
Ok(())
}
fn symlink_path(source: &Path, target: &Path) -> std::io::Result<()> {
if symlink_matches(source, target)? {
return Ok(());
}
if let Some(parent) = target.parent() {
ensure_dir(parent)?;
}
remove_path(target)?;
#[cfg(unix)]
{
std::os::unix::fs::symlink(source, target)
}
#[cfg(windows)]
{
if source.is_dir() {
std::os::windows::fs::symlink_dir(source, target)
} else {
std::os::windows::fs::symlink_file(source, target)
}
}
}
fn symlink_matches(source: &Path, target: &Path) -> std::io::Result<bool> {
let metadata = match std::fs::symlink_metadata(target) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(error) => return Err(error),
};
if !metadata.file_type().is_symlink() {
return Ok(false);
}
let linked = std::fs::read_link(target)?;
Ok(linked == source)
}
fn prune_stub_dir(dir: &Path, file_names: &[&str]) -> std::io::Result<()> {
let expected_files = file_names
.iter()
.map(|name| dir.join(name))
.collect::<FxHashSet<_>>();
prune_dir_entries(dir, &expected_files)
}
fn prune_runtime_node_modules(node_modules_dir: &Path) -> std::io::Result<()> {
let expected_files = FxHashSet::default();
let preserved_roots = ["vue", "vite", "@vue"]
.into_iter()
.map(|name| node_modules_dir.join(name))
.filter(|path| path.exists() || path.is_symlink())
.collect::<Vec<_>>();
super::materialize_fs::prune_unexpected_entries(
node_modules_dir,
&expected_files,
&preserved_roots,
)
}