use std::path::{Path, PathBuf};
use vize_carton::{FxHashSet, String as CompactString, profile};
use crate::batch::error::CorsaResult;
use crate::batch::materialize_fs::{
ensure_dir, ensure_materialize_root, prune_unexpected_entries, record_write_batch,
write_file_untracked, write_if_changed,
};
use crate::batch::runtime_deps::materialize_runtime_dependencies;
use super::{AUTO_IMPORT_STUBS_FILE, VUE_MODULE_STUBS_FILE, VirtualProject};
impl VirtualProject {
pub fn materialize(&self) -> CorsaResult<()> {
let expected_files = self.expected_materialized_files();
profile!(
"canon.project.prepare_dir",
ensure_materialize_root(&self.virtual_root)
)?;
profile!(
"canon.project.gc",
prune_unexpected_entries(
&self.virtual_root,
&expected_files,
&[self.virtual_root.join("node_modules")]
)
)?;
profile!(
"canon.project.runtime_deps",
materialize_runtime_dependencies(&self.project_root, &self.virtual_root)
)?;
profile!(
"canon.project.write_files",
(|| -> CorsaResult<()> {
let mut created_dirs: FxHashSet<&Path> = FxHashSet::default();
let mut write_calls = 0u64;
let mut written_bytes = 0u64;
for file in self.virtual_files.values() {
if let Some(parent) = file.virtual_path.parent()
&& created_dirs.insert(parent)
{
ensure_dir(parent)?;
}
write_file_untracked(&file.virtual_path, file.content.as_bytes())?;
write_calls += 1;
written_bytes += file.content.len() as u64;
}
for (virtual_path, original_path) in &self.passthrough_files {
if let Some(parent) = virtual_path.parent()
&& created_dirs.insert(parent)
{
ensure_dir(parent)?;
}
let content = std::fs::read(original_path)?;
write_file_untracked(virtual_path, &content)?;
write_calls += 1;
written_bytes += content.len() as u64;
}
record_write_batch(write_calls, written_bytes);
Ok(())
})()
)?;
profile!(
"canon.project.write_auto_imports",
self.write_auto_import_stubs()
)?;
profile!(
"canon.project.write_vue_module_stubs",
self.write_vue_module_stubs()
)?;
profile!(
"canon.project.write_tsconfig",
self.write_tsconfig_file(&self.virtual_root.join("tsconfig.json"), None, false)
)?;
Ok(())
}
pub fn write_declaration_tsconfig(
&self,
out_dir: &Path,
declaration_map: bool,
) -> CorsaResult<PathBuf> {
let config_path = self.virtual_root.join("tsconfig.declaration.json");
profile!(
"canon.project.write_dts_tsconfig",
self.write_tsconfig_file(&config_path, Some(out_dir), declaration_map)
)?;
Ok(config_path)
}
fn write_auto_import_stubs(&self) -> CorsaResult<()> {
if self.virtual_ts_options.auto_import_stubs.is_empty() {
return Ok(());
}
let capacity = self
.virtual_ts_options
.auto_import_stubs
.iter()
.fold(64usize, |acc, stub| acc + stub.len() + 1);
let mut content = CompactString::with_capacity(capacity);
content.push_str("// @ts-nocheck\n");
content.push_str("// Framework-provided globals for the virtual project.\n");
for stub in &self.virtual_ts_options.auto_import_stubs {
content.push_str(stub);
content.push('\n');
}
write_if_changed(
&self.virtual_root.join(AUTO_IMPORT_STUBS_FILE),
content.as_bytes(),
)?;
Ok(())
}
fn write_vue_module_stubs(&self) -> CorsaResult<()> {
let content = "// Vue SFC modules resolve through materialized .vue.ts files.\n";
write_if_changed(
&self.virtual_root.join(VUE_MODULE_STUBS_FILE),
content.as_bytes(),
)?;
Ok(())
}
fn expected_materialized_files(&self) -> FxHashSet<PathBuf> {
let mut files = FxHashSet::default();
files.reserve(self.virtual_files.len() + 3);
files.extend(self.virtual_files.keys().cloned());
files.extend(self.passthrough_files.keys().cloned());
if !self.virtual_ts_options.auto_import_stubs.is_empty() {
files.insert(self.virtual_root.join(AUTO_IMPORT_STUBS_FILE));
}
files.insert(self.virtual_root.join(VUE_MODULE_STUBS_FILE));
files.insert(self.virtual_root.join("tsconfig.json"));
files
}
pub(super) fn common_virtual_source_dir(&self) -> PathBuf {
let mut parents = self
.virtual_files
.keys()
.filter_map(|path| path.parent().map(Path::to_path_buf));
let Some(mut common) = parents.next() else {
return self.virtual_root.clone();
};
for parent in parents {
while !parent.starts_with(&common) {
if !common.pop() {
return self.virtual_root.clone();
}
}
}
common
}
pub(super) fn resolved_tsconfig_path(&self) -> Option<PathBuf> {
if let Some(ref tsconfig_path) = self.tsconfig_path {
return Some(tsconfig_path.clone());
}
let tsconfig = self.project_root.join("tsconfig.json");
tsconfig.exists().then_some(tsconfig)
}
}