use std::path::{Path, PathBuf};
use super::error::{TsgoError, TsgoResult};
use super::import_rewriter::ImportRewriter;
use super::source_map::CompositeSourceMap;
use super::virtual_ts::VirtualTsGenerator;
use super::SfcBlockType;
use oxc_span::SourceType;
use vize_carton::cstr;
use vize_carton::FxHashMap;
use vize_carton::String;
#[derive(Debug)]
pub struct VirtualFile {
pub content: String,
pub source_map: CompositeSourceMap,
pub original_path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct OriginalPosition {
pub path: PathBuf,
pub line: u32,
pub column: u32,
pub block_type: Option<SfcBlockType>,
}
pub struct VirtualProject {
project_root: PathBuf,
virtual_root: PathBuf,
virtual_files: FxHashMap<PathBuf, VirtualFile>,
generator: VirtualTsGenerator,
rewriter: ImportRewriter,
}
impl VirtualProject {
pub fn new(project_root: &Path) -> TsgoResult<Self> {
let virtual_root = project_root
.join("node_modules")
.join(".vize")
.join("canon");
Ok(Self {
project_root: project_root.to_path_buf(),
virtual_root,
virtual_files: FxHashMap::default(),
generator: VirtualTsGenerator::new(),
rewriter: ImportRewriter::new(),
})
}
pub fn project_root(&self) -> &Path {
&self.project_root
}
pub fn virtual_root(&self) -> &Path {
&self.virtual_root
}
pub fn register_vue_file(&mut self, path: &Path, content: &str) -> TsgoResult<()> {
let result = self
.generator
.generate_from_content(content)
.map_err(TsgoError::SfcParse)?;
let relative = path.strip_prefix(&self.project_root)?;
let mut virtual_path = self.virtual_root.join(relative);
let file_name = virtual_path
.file_name()
.and_then(|n| n.to_str())
.map(|n| cstr!("{n}.ts"))
.ok_or_else(|| TsgoError::PathError {
path: path.to_path_buf(),
})?;
virtual_path.set_file_name(file_name);
self.virtual_files.insert(
virtual_path,
VirtualFile {
content: result.code,
source_map: CompositeSourceMap::new(
Some(result.source_map),
super::import_rewriter::ImportSourceMap::empty(),
),
original_path: path.to_path_buf(),
},
);
Ok(())
}
pub fn register_ts_file(&mut self, path: &Path) -> TsgoResult<()> {
let content = std::fs::read_to_string(path)?;
let source_type = if path.extension().map(|e| e == "tsx").unwrap_or(false) {
SourceType::tsx()
} else {
SourceType::ts()
};
let result = self.rewriter.rewrite(&content, source_type);
let relative = path.strip_prefix(&self.project_root)?;
let virtual_path = self.virtual_root.join(relative);
self.virtual_files.insert(
virtual_path,
VirtualFile {
content: result.code,
source_map: CompositeSourceMap::new(None, result.source_map),
original_path: path.to_path_buf(),
},
);
Ok(())
}
pub fn materialize(&self) -> TsgoResult<()> {
if self.virtual_root.exists() {
std::fs::remove_dir_all(&self.virtual_root)?;
}
std::fs::create_dir_all(&self.virtual_root)?;
for (path, file) in &self.virtual_files {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, &file.content)?;
}
let tsconfig = self.generate_tsconfig()?;
std::fs::write(self.virtual_root.join("tsconfig.json"), tsconfig)?;
Ok(())
}
fn generate_tsconfig(&self) -> TsgoResult<String> {
let original_tsconfig = self.project_root.join("tsconfig.json");
let paths = if original_tsconfig.exists() {
self.extract_paths_from_tsconfig(&original_tsconfig)?
} else {
serde_json::json!({})
};
let config = serde_json::json!({
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": paths
},
"include": ["**/*.ts"],
"exclude": []
});
Ok(serde_json::to_string_pretty(&config)?.into())
}
fn extract_paths_from_tsconfig(&self, tsconfig_path: &Path) -> TsgoResult<serde_json::Value> {
let content = std::fs::read_to_string(tsconfig_path)?;
let config: serde_json::Value = serde_json::from_str(&content)?;
if let Some(paths) = config.get("compilerOptions").and_then(|c| c.get("paths")) {
return Ok(paths.clone());
}
Ok(serde_json::json!({}))
}
pub fn map_to_original(
&self,
virtual_path: &Path,
line: u32,
column: u32,
) -> Option<OriginalPosition> {
let file = self.virtual_files.get(virtual_path)?;
let virtual_offset = super::source_map::line_col_to_offset(&file.content, line, column)?;
let (orig_offset, _, block_type) = file.source_map.get_original_position(virtual_offset)?;
let original_content = std::fs::read_to_string(&file.original_path).ok()?;
let (orig_line, orig_col) =
super::source_map::offset_to_line_col(&original_content, orig_offset)?;
Some(OriginalPosition {
path: file.original_path.clone(),
line: orig_line,
column: orig_col,
block_type,
})
}
pub fn map_to_virtual(
&self,
original_path: &Path,
line: u32,
column: u32,
) -> Option<(PathBuf, u32, u32)> {
for (virtual_path, file) in &self.virtual_files {
if file.original_path == original_path {
let original_content = std::fs::read_to_string(&file.original_path).ok()?;
let sfc_offset =
super::source_map::line_col_to_offset(&original_content, line, column)?;
if let Some(ref sfc_map) = file.source_map.sfc_map {
for block_type in [
super::SfcBlockType::ScriptSetup,
super::SfcBlockType::Script,
super::SfcBlockType::Template,
] {
if let Some(virtual_offset) =
sfc_map.get_virtual_offset(sfc_offset, block_type)
{
let final_offset = file
.source_map
.import_map
.get_virtual_offset(virtual_offset);
if let Some((vline, vcol)) =
super::source_map::offset_to_line_col(&file.content, final_offset)
{
return Some((virtual_path.clone(), vline, vcol));
}
}
}
}
let final_offset = file.source_map.import_map.get_virtual_offset(sfc_offset);
if let Some((vline, vcol)) =
super::source_map::offset_to_line_col(&file.content, final_offset)
{
return Some((virtual_path.clone(), vline, vcol));
}
return Some((virtual_path.clone(), line, column));
}
}
None
}
pub fn file_count(&self) -> usize {
self.virtual_files.len()
}
pub fn is_empty(&self) -> bool {
self.virtual_files.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::VirtualProject;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_virtual_project_new() {
let temp_dir = TempDir::new().unwrap();
let project = VirtualProject::new(temp_dir.path()).unwrap();
assert_eq!(project.project_root(), temp_dir.path());
assert!(project.virtual_root().ends_with("node_modules/.vize/canon"));
}
#[test]
fn test_register_vue_file() {
let temp_dir = TempDir::new().unwrap();
let mut project = VirtualProject::new(temp_dir.path()).unwrap();
let vue_content = r#"<template>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
const message = 'Hello'
</script>
"#;
let src_dir = temp_dir.path().join("src");
fs::create_dir_all(&src_dir).unwrap();
let vue_path = src_dir.join("App.vue");
fs::write(&vue_path, vue_content).unwrap();
project.register_vue_file(&vue_path, vue_content).unwrap();
assert_eq!(project.file_count(), 1);
}
#[test]
fn test_materialize() {
let temp_dir = TempDir::new().unwrap();
let mut project = VirtualProject::new(temp_dir.path()).unwrap();
let vue_content = r#"<template>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
const message = 'Hello'
</script>
"#;
let src_dir = temp_dir.path().join("src");
fs::create_dir_all(&src_dir).unwrap();
let vue_path = src_dir.join("App.vue");
fs::write(&vue_path, vue_content).unwrap();
project.register_vue_file(&vue_path, vue_content).unwrap();
project.materialize().unwrap();
let virtual_file = temp_dir
.path()
.join("node_modules/.vize/canon/src/App.vue.ts");
assert!(virtual_file.exists());
let tsconfig = temp_dir
.path()
.join("node_modules/.vize/canon/tsconfig.json");
assert!(tsconfig.exists());
}
}