vize_canon 0.193.0

Canon - The standard of correctness for Vize type checking
Documentation
//! Materializing the virtual project to disk so Corsa can observe it as a real
//! TypeScript project: writing virtual files, passthrough modules, ambient stub
//! `.d.ts` files, and pruning stale entries from a previous run.

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 {
    /// Materialize the virtual project to disk for diagnostics collection.
    ///
    /// The materialized tree is a cache, but Corsa observes it as a real project.
    /// We therefore prune only entries outside the expected file/dir set and
    /// preserve nested runtime dependencies under `node_modules`. File writes are
    /// batched with directory creation de-duplicated per parent path; tsconfig and
    /// other stable control files still use `write_if_changed` because touching
    /// them can invalidate TypeScript's own filesystem caches.
    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(())
    }

    /// Write a declaration-emitting tsconfig and return its path.
    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)
    }
}