use super::import_rewriter::ImportRewriter;
use super::source_map::SfcSourceMap;
use super::SfcBlockType;
use vize_atelier_sfc::SfcDescriptor;
use vize_carton::append;
use vize_carton::cstr;
use vize_carton::String;
use vize_croquis::{Analyzer, AnalyzerOptions, Croquis};
#[derive(Debug)]
pub struct VirtualTsResult {
pub code: String,
pub source_map: SfcSourceMap,
}
const VUE_SETUP_COMPILER_MACROS: &str = r#"// Compiler macros (transformed at compile time by Vue)
function defineProps<T>(): T { return undefined as unknown as T; }
type __BatchEmitFn<T> = T extends (...args: any[]) => any ? T : (<K extends keyof T>(event: K, ...args: T[K] extends any[] ? T[K] : any[]) => void);
function defineEmits<T>(): __BatchEmitFn<T> { return (() => {}) as any; }
function defineEmits<T extends readonly string[]>(_events: T): (event: T[number], ...args: any[]) => void { void _events; return (() => {}) as any; }
function defineEmits<T extends Record<string, any>>(_events: T): (event: keyof T, ...args: any[]) => void { void _events; return (() => {}) as any; }
function defineExpose<T>(_exposed?: T): void { void _exposed; }
function defineModel<T>(): $Vue['Ref']<T | undefined> { return undefined as unknown as $Vue['Ref']<T | undefined>; }
function defineModel<T>(_options: any): $Vue['Ref']<T> { void _options; return undefined as unknown as $Vue['Ref']<T>; }
function defineModel<T>(_name: string, _options?: any): $Vue['Ref']<T> { void _name; void _options; return undefined as unknown as $Vue['Ref']<T>; }
function defineSlots<T>(): T { return undefined as unknown as T; }
function withDefaults<T, D>(_props: T, _defaults: D): T & D { void _props; void _defaults; return undefined as unknown as T & D; }
function useTemplateRef<T = any>(_key: string): $Vue['ShallowRef']<T | null> { void _key; return undefined as unknown as $Vue['ShallowRef']<T | null>; }
// Mark compiler macros as used
void defineProps; void defineEmits; void defineExpose; void defineModel; void defineSlots; void withDefaults; void useTemplateRef;
"#;
const VUE_TEMPLATE_CONTEXT: &str = r#"// Vue instance context (available in template)
const $attrs: Record<string, unknown> = {} as any;
const $slots: Record<string, (...args: any[]) => any> = {} as any;
const $refs: Record<string, any> = {} as any;
const $emit: (...args: any[]) => void = (() => {}) as any;
// Mark template context as used
void $attrs; void $slots; void $refs; void $emit;
"#;
pub struct VirtualTsGenerator;
impl VirtualTsGenerator {
pub fn new() -> Self {
Self
}
pub fn generate(&self, descriptor: &SfcDescriptor, analysis: &Croquis) -> VirtualTsResult {
let mut code = String::default();
let mut source_map = SfcSourceMap::new();
code.push_str("// ============================================\n");
code.push_str("// Virtual TypeScript for Vue SFC Type Checking\n");
code.push_str("// Generated by vize\n");
code.push_str("// ============================================\n\n");
code.push_str("type $Vue = import('vue');\n\n");
code.push_str(VUE_SETUP_COMPILER_MACROS);
code.push('\n');
code.push_str(VUE_TEMPLATE_CONTEXT);
code.push('\n');
code.push_str("// ========== Imports ==========\n");
if let Some(ref script_setup) = descriptor.script_setup {
self.emit_imports(
&mut code,
&script_setup.content,
&mut source_map,
&script_setup.loc,
);
} else if let Some(ref script) = descriptor.script {
self.emit_imports(&mut code, &script.content, &mut source_map, &script.loc);
}
code.push('\n');
code.push_str("// ========== Script Content ==========\n");
if let Some(ref script_setup) = descriptor.script_setup {
let script_start = code.len() as u32;
self.emit_script_content_module_scope(&mut code, &script_setup.content);
let script_end = code.len() as u32;
source_map.add_mapping(
script_start,
script_end,
script_setup.loc.start as u32,
SfcBlockType::ScriptSetup,
);
} else if let Some(ref script) = descriptor.script {
let script_start = code.len() as u32;
self.emit_script_content_module_scope(&mut code, &script.content);
let script_end = code.len() as u32;
source_map.add_mapping(
script_start,
script_end,
script.loc.start as u32,
SfcBlockType::Script,
);
}
code.push('\n');
if let Some(ref template) = descriptor.template {
code.push_str("// ========== Template Bindings ==========\n");
code.push_str("(function __template() {\n");
let template_start = code.len() as u32;
self.emit_template_bindings(&mut code, analysis);
let template_end = code.len() as u32;
source_map.add_mapping(
template_start,
template_end,
template.loc.start as u32,
SfcBlockType::Template,
);
code.push_str("})();\n\n");
}
code.push_str("// ========== Component Export ==========\n");
code.push_str("import { DefineComponent } from 'vue';\n\n");
self.emit_component_types(&mut code, analysis);
code.push_str("declare const __component: DefineComponent<__Props, {}, {}, {}, {}, {}, {}, __Emits>;\n");
code.push_str("export default __component;\n");
VirtualTsResult { code, source_map }
}
fn emit_imports(
&self,
code: &mut String,
script: &str,
_source_map: &mut SfcSourceMap,
_loc: &vize_atelier_sfc::BlockLocation,
) {
let rewriter = ImportRewriter::new();
let rewrite_result = rewriter.rewrite(script, oxc_span::SourceType::ts());
for line in rewrite_result.code.lines() {
let trimmed = line.trim();
if trimmed.starts_with("import ") {
code.push_str(" ");
code.push_str(line);
code.push('\n');
}
}
}
fn emit_script_content_module_scope(&self, code: &mut String, script: &str) {
let mut first_content = true;
for line in script.lines() {
let trimmed = line.trim();
if trimmed.starts_with("import ") {
continue;
}
if first_content && trimmed.is_empty() {
continue;
}
first_content = false;
code.push_str(line);
code.push('\n');
}
}
fn emit_template_bindings(&self, code: &mut String, analysis: &Croquis) {
for (name, _binding_type) in analysis.bindings.iter() {
append!(*code, " void {name};\n");
}
}
fn emit_component_types(&self, code: &mut String, analysis: &Croquis) {
code.push_str("export interface __Props {\n");
for prop in analysis.macros.props() {
let optional = if !prop.required { "?" } else { "" };
let prop_type = prop.prop_type.as_deref().unwrap_or("unknown");
append!(*code, " {}{}: {};\n", prop.name, optional, prop_type);
}
code.push_str("}\n\n");
code.push_str("export interface __Emits {\n");
for emit in analysis.macros.emits() {
if let Some(ref payload_type) = emit.payload_type {
append!(
*code,
" (e: '{}', payload: {}): void;\n",
emit.name,
payload_type
);
} else {
append!(*code, " (e: '{}'): void;\n", emit.name);
}
}
code.push_str("}\n\n");
}
pub fn generate_from_content(&self, content: &str) -> Result<VirtualTsResult, String> {
let options = vize_atelier_sfc::SfcParseOptions::default();
let descriptor =
vize_atelier_sfc::parse_sfc(content, options).map_err(|e| cstr!("{e:?}"))?;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
if let Some(ref script_setup) = descriptor.script_setup {
analyzer.analyze_script_setup(&script_setup.content);
} else if let Some(ref script) = descriptor.script {
analyzer.analyze_script_plain(&script.content);
}
if let Some(ref template) = descriptor.template {
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, &template.content);
analyzer.analyze_template(&root);
}
let analysis = analyzer.finish();
Ok(self.generate(&descriptor, &analysis))
}
}
impl Default for VirtualTsGenerator {
fn default() -> Self {
Self
}
}
#[cfg(test)]
mod tests {
use super::VirtualTsGenerator;
#[test]
fn test_generate_from_content() {
let generator = VirtualTsGenerator::new();
let content = r#"<template>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
const message = ref('Hello')
</script>
"#;
let result = generator.generate_from_content(content);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.code.contains("./Child.vue.ts"));
assert!(result.code.contains("function defineProps"));
assert!(result.code.contains("function __template()"));
}
#[test]
fn test_rewrite_import_line() {
let rewriter = super::super::import_rewriter::ImportRewriter::new();
let result = rewriter.rewrite("import App from './App.vue'", oxc_span::SourceType::ts());
assert_eq!(result.code, "import App from './App.vue.ts'");
let result = rewriter.rewrite("import { ref } from 'vue'", oxc_span::SourceType::ts());
assert_eq!(result.code, "import { ref } from 'vue'");
}
}