use super::import_rewriter::ImportRewriter;
use super::source_map::{SfcBlockRange, SfcSourceMap};
use super::SfcBlockType;
use crate::virtual_ts::VizeMapping;
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>; }
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;
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 mappings = Vec::new();
let mut blocks = Vec::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);
} else if let Some(ref script) = descriptor.script {
self.emit_imports(&mut code, &script.content);
}
code.push('\n');
code.push_str("// ========== Script Content ==========\n");
if let Some(ref script_setup) = descriptor.script_setup {
let start = code.len();
self.emit_script_content_module_scope(&mut code, &script_setup.content);
let end = code.len();
mappings.push(VizeMapping {
gen_range: start..end,
src_range: script_setup.loc.start
..script_setup.loc.start + script_setup.content.len(),
});
blocks.push(SfcBlockRange {
start: script_setup.loc.start as u32,
end: script_setup.loc.start as u32 + script_setup.content.len() as u32,
block_type: SfcBlockType::ScriptSetup,
});
} else if let Some(ref script) = descriptor.script {
let start = code.len();
self.emit_script_content_module_scope(&mut code, &script.content);
let end = code.len();
mappings.push(VizeMapping {
gen_range: start..end,
src_range: script.loc.start..script.loc.start + script.content.len(),
});
blocks.push(SfcBlockRange {
start: script.loc.start as u32,
end: script.loc.start as u32 + script.content.len() as u32,
block_type: 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 start = code.len();
self.emit_template_bindings(&mut code, analysis);
let end = code.len();
mappings.push(VizeMapping {
gen_range: start..end,
src_range: template.loc.start..template.loc.start + template.content.len(),
});
blocks.push(SfcBlockRange {
start: template.loc.start as u32,
end: template.loc.start as u32 + template.content.len() as u32,
block_type: 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: SfcSourceMap::new(mappings, blocks),
}
}
fn emit_imports(&self, code: &mut String, script: &str) {
let rewritten = ImportRewriter::new().rewrite(script, oxc_span::SourceType::ts());
for line in rewritten.code.lines() {
if line.trim().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, _) 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 descriptor =
vize_atelier_sfc::parse_sfc(content, vize_atelier_sfc::SfcParseOptions::default())
.map_err(|error| cstr!("{error:?}"))?;
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);
}
Ok(self.generate(&descriptor, &analyzer.finish()))
}
}
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).unwrap();
insta::assert_snapshot!(result.code.as_str());
}
}