use base64::{engine::general_purpose::STANDARD, Engine};
use sha2::{Digest, Sha256};
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
use crate::{
code_transform::SourceMapOptions,
codegen::vue::{
plugin::VueCodegenPlugin, script::extract_binding_metadata,
template::types::BindingMetadata,
},
cursor::ScriptDetector,
syntax::{
plugin::{SyntaxPlugin, SyntaxPluginContext, SyntaxPluginOptions},
plugins::{
analysis::Analysis, css_parser::CssParserPlugin,
oxc_parser::oxc_parser::OxcParserPlugin,
},
syntax::Syntax,
},
tokenizer::byte::tokenize,
utils::oxc::vue::{parse_script, ScriptMode},
};
pub struct CodegenResult {
pub code: String,
pub source_map: String,
pub code_with_source_map: String,
pub duration_ms: f64,
}
#[derive(Debug, Clone)]
pub struct FeatureFlags {
pub options_api: bool,
pub props_destructure: bool,
}
impl Default for FeatureFlags {
fn default() -> Self {
Self {
options_api: true,
props_destructure: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CodegenOptions {
pub filename: Option<String>,
pub include_source_content: bool,
pub ssr: bool,
pub is_production: bool,
pub component_id: Option<String>,
pub features: FeatureFlags,
}
impl CodegenOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
self.filename = Some(filename.into());
self
}
pub fn include_source_content(mut self, include: bool) -> Self {
self.include_source_content = include;
self
}
pub fn ssr(mut self, ssr: bool) -> Self {
self.ssr = ssr;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct ViteCodegenOptions {
pub filename: Option<String>,
pub ssr: bool,
pub is_production: bool,
pub component_id: Option<String>,
pub sourcemap: bool,
}
#[derive(Debug, Clone)]
pub struct ViteCodegenResult {
pub script: Option<BlockOutput>,
pub template: Option<BlockOutput>,
pub styles: Vec<StyleBlock>,
pub custom: Vec<CustomBlock>,
pub duration_ms: f64,
}
#[derive(Debug, Clone)]
pub struct BlockOutput {
pub code: String,
pub source_map: Option<String>,
pub imports: Vec<BlockImport>,
pub body_start: u32,
}
#[derive(Debug, Clone)]
pub struct BlockImport {
pub source: String,
pub specifiers: Vec<String>,
pub start: u32,
pub end: u32,
}
#[derive(Debug, Clone)]
pub struct StyleBlock {
pub code: String,
pub source_map: Option<String>,
pub scoped: bool,
pub is_module: bool,
pub lang: Option<String>,
pub module_name: Option<String>,
pub module_classes: Vec<(String, String)>,
}
#[derive(Debug, Clone)]
pub struct CustomBlock {
pub tag: String,
pub content: String,
pub attrs: Vec<(String, String)>,
pub start_utf16: u32,
pub end_utf16: u32,
}
fn extract_imports(code: &str) -> (Vec<BlockImport>, u32) {
let mut imports = Vec::new();
let mut body_start: u32 = 0;
let bytes = code.as_bytes();
let len = bytes.len();
let mut pos = 0;
while pos < len {
while pos < len && matches!(bytes[pos], b' ' | b'\n' | b'\r' | b'\t') {
pos += 1;
}
if pos >= len {
break;
}
if pos + 6 <= len && &bytes[pos..pos + 6] == b"import" {
let import_start = pos as u32;
let mut end = pos;
while end < len && bytes[end] != b'\n' {
end += 1;
}
if end < len && bytes[end] == b'\n' {
end += 1;
}
let import_line = &code[pos..end];
let import_end = end as u32;
if let Some(from_idx) = import_line.find("from ") {
let after_from = &import_line[from_idx + 5..];
let quote = after_from.chars().next().unwrap_or('"');
if quote == '"' || quote == '\'' {
if let Some(end_quote) = after_from[1..].find(quote) {
let source = after_from[1..1 + end_quote].to_string();
let mut specifiers = Vec::new();
if let Some(open_brace) = import_line.find('{') {
if let Some(close_brace) = import_line.find('}') {
let spec_str = &import_line[open_brace + 1..close_brace];
for spec in spec_str.split(',') {
let spec = spec.trim();
if !spec.is_empty() {
specifiers.push(spec.to_string());
}
}
}
}
imports.push(BlockImport {
source,
specifiers,
start: import_start,
end: import_end,
});
}
}
}
pos = end;
} else {
body_start = pos as u32;
break;
}
}
if body_start == 0 && !imports.is_empty() {
body_start = code.len() as u32;
}
(imports, body_start)
}
pub fn get_hash(text: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(text.as_bytes());
let result = hasher.finalize();
hex::encode(&result[..4])
}
pub fn generate_component_id(options: &CodegenOptions, source: &str) -> String {
if let Some(ref id) = options.component_id {
return id.clone();
}
let filename = options.filename.as_deref().unwrap_or("component.vue");
let normalized = filename.replace('\\', "/");
if options.is_production {
get_hash(&normalized)
} else {
get_hash(&format!("{}{}", normalized, source))
}
}
fn has_scoped_style(source: &[u8]) -> bool {
let style_tag = b"<style";
let scoped = b"scoped";
let close = b">";
let mut pos = 0;
while pos + style_tag.len() < source.len() {
if let Some(style_start) = find_bytes(&source[pos..], style_tag) {
let style_pos = pos + style_start;
if let Some(close_offset) = find_bytes(&source[style_pos..], close) {
let tag_content = &source[style_pos..style_pos + close_offset];
if find_bytes(tag_content, scoped).is_some() {
return true;
}
pos = style_pos + close_offset + 1;
} else {
break;
}
} else {
break;
}
}
false
}
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
fn pre_scan_script_setup_bindings(
source: &str,
bytes: &[u8],
allocator: &oxc_allocator::Allocator,
source_type: oxc_span::SourceType,
) -> Option<BindingMetadata> {
let script_tag = b"<script";
let setup_attr = b"setup";
let close_tag = b"</script>";
let mut pos = 0;
while pos + script_tag.len() < bytes.len() {
let script_start = find_bytes(&bytes[pos..], script_tag)?;
let script_pos = pos + script_start;
let after_tag = script_pos + script_tag.len();
match bytes.get(after_tag) {
Some(b' ') | Some(b'\t') | Some(b'\n') | Some(b'\r') | Some(b'>') => {}
_ => {
pos = after_tag;
continue;
}
}
let tag_close = find_bytes(&bytes[after_tag..], b">")?;
let tag_content = &bytes[after_tag..after_tag + tag_close];
if find_bytes(tag_content, setup_attr).is_none() {
pos = after_tag + tag_close + 1;
continue;
}
let content_start = after_tag + tag_close + 1;
let content_end_offset = find_bytes(&bytes[content_start..], close_tag)?;
let content_end = content_start + content_end_offset;
let script_content = &source[content_start..content_end];
let parser = oxc_parser::Parser::new(allocator, script_content, source_type);
let ret = parser.parse();
let parsed = parse_script(
&ret.program,
ScriptMode::Setup,
content_start as u32,
script_content,
);
return Some(extract_binding_metadata(&parsed));
}
None
}
fn extract_component_name(filename: &str) -> String {
let name = filename.rsplit(['/', '\\']).next().unwrap_or(filename);
let name = name.strip_suffix(".vue").unwrap_or(name);
let name = name.strip_suffix(".ts").unwrap_or(name);
let name = name.strip_suffix(".js").unwrap_or(name);
name.to_string()
}
pub fn generate(
input: &str,
options: &CodegenOptions,
allocator: &oxc_allocator::Allocator,
) -> CodegenResult {
let start = Instant::now();
let bytes = input.as_bytes();
let syntax_options: &'static SyntaxPluginOptions =
Box::leak(Box::new(SyntaxPluginOptions::default()));
let mut syntax_context = SyntaxPluginContext::new(input, bytes, syntax_options);
let script_detector = ScriptDetector::new();
let detected = script_detector.detect(bytes);
let mut oxc_parser = OxcParserPlugin::new(allocator, detected.language.to_source_type());
let mut vue_codegen = VueCodegenPlugin::new(input, allocator);
if let Some(ref filename) = options.filename {
let component_name = extract_component_name(filename);
vue_codegen.set_component_name(&component_name);
}
vue_codegen.set_production(options.is_production);
if has_scoped_style(bytes) {
let component_name = options
.filename
.as_ref()
.map(|f| extract_component_name(f))
.unwrap_or_else(|| "App".to_string());
let hash = get_hash(&component_name);
let hash_bytes = hash.as_bytes();
let mut scope_id = [0u8; 8];
scope_id.copy_from_slice(&hash_bytes[..8.min(hash_bytes.len())]);
vue_codegen.set_scope_id(scope_id);
}
if let Some(metadata) =
pre_scan_script_setup_bindings(input, bytes, allocator, detected.language.to_source_type())
{
vue_codegen.set_binding_metadata(metadata);
}
let mut analysis = Analysis::new();
let mut css_parser = CssParserPlugin::new();
{
let pipeline: Vec<&mut dyn SyntaxPlugin> = vec![
&mut css_parser,
&mut oxc_parser,
&mut analysis,
&mut vue_codegen,
];
let mut syntax = Syntax::new(pipeline);
syntax.start(&mut syntax_context);
tokenize(bytes, |e| {
syntax.handle(&e, &mut syntax_context);
});
syntax.end(&mut syntax_context);
}
let mut code = vue_codegen.get_code();
if !options.features.options_api {
code = format!("/* __VUE_OPTIONS_API__: false */\n{}", code);
}
if !options.features.props_destructure {
code = format!("/* __VUE_PROPS_DESTRUCTURE__: false */\n{}", code);
}
let source_map_options = SourceMapOptions::new()
.with_source(
options
.filename
.clone()
.unwrap_or_else(|| "input.vue".to_string()),
)
.with_file(
options
.filename
.as_ref()
.map(|f| format!("{}.js", f))
.unwrap_or_else(|| "output.js".to_string()),
)
.include_content(options.include_source_content);
let source_map = vue_codegen.generate_source_map(source_map_options);
let source_map_base64 = STANDARD.encode(&source_map);
let code_with_source_map = format!(
"{}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,{}",
code, source_map_base64
);
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
CodegenResult {
code,
source_map,
code_with_source_map,
duration_ms,
}
}
pub fn generate_for_vite(
input: &str,
options: &ViteCodegenOptions,
allocator: &oxc_allocator::Allocator,
) -> ViteCodegenResult {
use crate::codegen::vue::style_plugin::StyleCodegenPlugin;
let start = Instant::now();
let bytes = input.as_bytes();
let script_detector = ScriptDetector::new();
let detected = script_detector.detect(bytes);
let component_name = options
.filename
.as_ref()
.map(|f| extract_component_name(f))
.unwrap_or_else(|| "App".to_string());
let has_script_setup =
pre_scan_script_setup_bindings(input, bytes, allocator, detected.language.to_source_type());
let use_inline_template = options.is_production && has_script_setup.is_some();
let syntax_options: &'static SyntaxPluginOptions =
Box::leak(Box::new(SyntaxPluginOptions::default()));
let mut syntax_context = SyntaxPluginContext::new(input, bytes, syntax_options);
let mut oxc_parser = OxcParserPlugin::new(allocator, detected.language.to_source_type());
let mut analysis = Analysis::new();
let mut css_parser = CssParserPlugin::new();
let mut style_codegen = StyleCodegenPlugin::new(input, allocator, &component_name);
let scope_id = if has_scoped_style(bytes) {
let hash = match &options.component_id {
Some(id) => id.clone(),
None => get_hash(&component_name),
};
let hash_bytes = hash.as_bytes();
let mut scope_id = [0u8; 8];
scope_id.copy_from_slice(&hash_bytes[..8.min(hash_bytes.len())]);
style_codegen.set_scope_id(scope_id);
Some(scope_id)
} else {
None
};
let source_name = options
.filename
.clone()
.unwrap_or_else(|| "input.vue".to_string());
let (script_output, template_output) = if use_inline_template {
let mut vue_codegen = VueCodegenPlugin::new(input, allocator);
vue_codegen.set_component_name(&component_name);
vue_codegen.set_production(options.is_production);
if let Some(sid) = scope_id {
vue_codegen.set_scope_id(sid);
}
if let Some(ref metadata) = has_script_setup {
vue_codegen.set_binding_metadata(metadata.clone());
}
{
let pipeline: Vec<&mut dyn SyntaxPlugin> = vec![
&mut css_parser,
&mut oxc_parser,
&mut analysis,
&mut vue_codegen,
&mut style_codegen,
];
let mut syntax = Syntax::new(pipeline);
syntax.start(&mut syntax_context);
tokenize(bytes, |e| {
syntax.handle(&e, &mut syntax_context);
});
syntax.end(&mut syntax_context);
}
let code = vue_codegen.get_code();
let source_map = if options.sourcemap {
let sm_options = SourceMapOptions::new()
.with_source(source_name.clone())
.with_file(format!("{}.script.js", source_name))
.include_content(true);
Some(vue_codegen.generate_source_map(sm_options))
} else {
None
};
let (imports, body_start) = extract_imports(&code);
let script = Some(BlockOutput {
code,
source_map,
imports,
body_start,
});
(script, None)
} else {
use crate::codegen::vue::script_plugin::ScriptCodegenPlugin;
use crate::codegen::vue::template_plugin::TemplateCodegenPlugin;
let mut script_codegen = ScriptCodegenPlugin::new(input, allocator);
let mut template_codegen = TemplateCodegenPlugin::new(input, allocator);
script_codegen.set_component_name(&component_name);
script_codegen.set_production(options.is_production);
template_codegen.set_production(options.is_production);
if let Some(sid) = scope_id {
script_codegen.set_scope_id(sid);
template_codegen.set_scope_id(sid);
}
if let Some(ref metadata) = has_script_setup {
template_codegen.set_binding_metadata(metadata.clone());
}
{
let pipeline: Vec<&mut dyn SyntaxPlugin> = vec![
&mut css_parser,
&mut oxc_parser,
&mut analysis,
&mut script_codegen,
&mut template_codegen,
&mut style_codegen,
];
let mut syntax = Syntax::new(pipeline);
syntax.start(&mut syntax_context);
tokenize(bytes, |e| {
syntax.handle(&e, &mut syntax_context);
});
syntax.end(&mut syntax_context);
}
let script = if script_codegen.has_script() {
let code = script_codegen.get_code();
let source_map = if options.sourcemap {
let sm_options = SourceMapOptions::new()
.with_source(source_name.clone())
.with_file(format!("{}.script.js", source_name))
.include_content(true);
Some(script_codegen.generate_source_map(sm_options))
} else {
None
};
let (imports, body_start) = extract_imports(&code);
Some(BlockOutput {
code,
source_map,
imports,
body_start,
})
} else {
Some(BlockOutput {
code: "export default {};\n".to_string(),
source_map: None,
imports: vec![],
body_start: 0,
})
};
let template = if template_codegen.has_template() {
let code = template_codegen.get_code();
let source_map = if options.sourcemap {
let sm_options = SourceMapOptions::new()
.with_source(source_name.clone())
.with_file(format!("{}.template.js", source_name))
.include_content(true);
Some(template_codegen.generate_source_map(sm_options))
} else {
None
};
let (imports, body_start) = extract_imports(&code);
Some(BlockOutput {
code,
source_map,
imports,
body_start,
})
} else {
None
};
(script, template)
};
let styles: Vec<StyleBlock> = style_codegen
.styles
.iter()
.map(|s| {
let code = s.get_code();
let source_map = if options.sourcemap {
let sm_options = SourceMapOptions::new()
.with_source(source_name.clone())
.with_file(format!("{}.style.css", source_name))
.include_content(true);
Some(s.generate_source_map(sm_options))
} else {
None
};
StyleBlock {
code,
source_map,
scoped: s.scoped,
is_module: s.is_module,
lang: s.lang.clone(),
module_name: s.module_name.clone(),
module_classes: s.module_classes.clone(),
}
})
.collect();
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
ViteCodegenResult {
script: script_output,
template: template_output,
styles,
custom: vec![],
duration_ms,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_component_name_simple() {
assert_eq!(extract_component_name("App.vue"), "App");
assert_eq!(extract_component_name("my-component.vue"), "my-component");
assert_eq!(extract_component_name("MyComponent.vue"), "MyComponent");
}
#[test]
fn test_extract_component_name_with_path() {
assert_eq!(
extract_component_name("src/components/MyComponent.vue"),
"MyComponent"
);
assert_eq!(extract_component_name("components/Button.vue"), "Button");
assert_eq!(
extract_component_name("C:\\Users\\dev\\project\\App.vue"),
"App"
);
}
#[test]
fn test_extract_component_name_no_extension() {
assert_eq!(extract_component_name("MyComponent"), "MyComponent");
assert_eq!(extract_component_name("src/App"), "App");
}
#[test]
fn test_generate_simple_template() {
let source = "<template><div>Hello</div></template>";
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
assert!(!result.code.is_empty());
assert!(result.source_map.starts_with('{'));
assert!(result
.code_with_source_map
.contains("//# sourceMappingURL=data:application/json"));
}
#[test]
fn test_generate_with_interpolation() {
let source = "<template><div>{{ message }}</div></template>";
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new()
.with_filename("app.vue")
.include_source_content(true);
let result = generate(source, &options, &allocator);
assert!(!result.code.is_empty());
assert!(!result.source_map.is_empty());
}
#[test]
fn test_generate_complex_component() {
let source = r#"<template>
<div v-if="show" class="container">
<span v-for="item in items" :key="item.id">
{{ item.name }}
</span>
</div>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("complex.vue");
let result = generate(source, &options, &allocator);
assert!(!result.code.is_empty());
assert!(result.code_with_source_map.contains("base64,"));
}
#[test]
fn test_codegen_options_is_production_default() {
let options = CodegenOptions::default();
assert!(
!options.is_production,
"Default should be development mode (is_production = false)"
);
}
#[test]
fn test_codegen_options_features_default() {
let options = CodegenOptions::default();
assert!(
options.features.options_api,
"Options API should be enabled by default"
);
assert!(
options.features.props_destructure,
"Props destructure should be enabled by default"
);
}
#[test]
fn test_codegen_options_custom_component_id() {
let options = CodegenOptions {
component_id: Some("custom-123".to_string()),
..Default::default()
};
assert_eq!(
options.component_id,
Some("custom-123".to_string()),
"Custom component ID should be stored"
);
}
#[test]
fn test_codegen_options_is_production_can_be_set() {
let options = CodegenOptions {
is_production: true,
..Default::default()
};
assert!(
options.is_production,
"is_production should be settable to true"
);
}
#[test]
fn test_codegen_options_features_can_be_disabled() {
let options = CodegenOptions {
features: FeatureFlags {
options_api: false,
props_destructure: false,
},
..Default::default()
};
assert!(
!options.features.options_api,
"options_api should be disableable"
);
assert!(
!options.features.props_destructure,
"props_destructure should be disableable"
);
}
#[test]
fn test_component_id_hash_length() {
let hash = get_hash("test");
assert_eq!(hash.len(), 8, "Hash should be 8 characters");
}
#[test]
fn test_component_id_hash_deterministic() {
let hash1 = get_hash("test");
let hash2 = get_hash("test");
assert_eq!(hash1, hash2, "Same input should produce same hash");
}
#[test]
fn test_component_id_hash_different_inputs() {
let hash1 = get_hash("input1");
let hash2 = get_hash("input2");
assert_ne!(
hash1, hash2,
"Different inputs should produce different hashes"
);
}
#[test]
fn test_component_id_production_vs_dev() {
let source = "<template></template>";
let prod_options = CodegenOptions {
filename: Some("App.vue".to_string()),
is_production: true,
..Default::default()
};
let dev_options = CodegenOptions {
filename: Some("App.vue".to_string()),
is_production: false,
..Default::default()
};
let prod_id = generate_component_id(&prod_options, source);
let dev_id = generate_component_id(&dev_options, source);
assert_ne!(prod_id, dev_id, "Production and dev IDs should differ");
}
#[test]
fn test_component_id_custom_override() {
let options = CodegenOptions {
component_id: Some("my-custom-id".to_string()),
..Default::default()
};
let id = generate_component_id(&options, "any source");
assert_eq!(id, "my-custom-id", "Custom ID should override generation");
}
#[test]
fn test_component_id_normalizes_windows_paths() {
let source = "<template></template>";
let unix_options = CodegenOptions {
filename: Some("src/components/App.vue".to_string()),
is_production: true,
..Default::default()
};
let windows_options = CodegenOptions {
filename: Some("src\\components\\App.vue".to_string()),
is_production: true,
..Default::default()
};
let unix_id = generate_component_id(&unix_options, source);
let windows_id = generate_component_id(&windows_options, source);
assert_eq!(
unix_id, windows_id,
"Unix and Windows paths should produce same ID"
);
}
#[test]
fn test_options_api_disabled_adds_marker() {
let source = r#"<template><div>Test</div></template>
<script setup>
const msg = 'hello'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions {
features: FeatureFlags {
options_api: false,
..Default::default()
},
..Default::default()
};
let result = generate(source, &options, &allocator);
assert!(
result.code.contains("/* __VUE_OPTIONS_API__: false */"),
"When options_api is disabled, should add marker comment. Generated:\n{}",
result.code
);
}
#[test]
fn test_options_api_enabled_no_marker() {
let source = r#"<template><div>Test</div></template>
<script setup>
const msg = 'hello'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions {
features: FeatureFlags {
options_api: true,
..Default::default()
},
..Default::default()
};
let result = generate(source, &options, &allocator);
assert!(
!result.code.contains("/* __VUE_OPTIONS_API__: false */"),
"When options_api is enabled, should NOT have marker. Generated:\n{}",
result.code
);
}
#[test]
fn test_props_destructure_disabled_adds_marker() {
let source = r#"<template><div>{{ foo }}</div></template>
<script setup>
const props = defineProps<{ foo: string }>()
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions {
features: FeatureFlags {
props_destructure: false,
..Default::default()
},
..Default::default()
};
let result = generate(source, &options, &allocator);
assert!(
result
.code
.contains("/* __VUE_PROPS_DESTRUCTURE__: false */"),
"When props_destructure is disabled, should add marker comment. Generated:\n{}",
result.code
);
}
#[test]
fn test_props_destructure_enabled_no_marker() {
let source = r#"<template><div>{{ foo }}</div></template>
<script setup>
const props = defineProps<{ foo: string }>()
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions {
features: FeatureFlags {
props_destructure: true,
..Default::default()
},
..Default::default()
};
let result = generate(source, &options, &allocator);
assert!(
!result
.code
.contains("/* __VUE_PROPS_DESTRUCTURE__: false */"),
"When props_destructure is enabled, should NOT have marker. Generated:\n{}",
result.code
);
}
#[test]
fn test_production_setup_no_expose_when_not_used() {
let source = r#"<template><div>{{ msg }}</div></template>
<script setup>
const msg = 'Hello'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions {
is_production: true,
..Default::default()
};
let result = generate(source, &options, &allocator);
assert!(
result.code.contains("setup(__props)"),
"Production should have minimal setup signature without expose. Generated:\n{}",
result.code
);
assert!(
!result.code.contains("expose: __expose"),
"Production should not have __expose in signature. Generated:\n{}",
result.code
);
assert!(
!result.code.contains("__expose()"),
"Production should not auto-call __expose(). Generated:\n{}",
result.code
);
}
#[test]
fn test_production_inline_render_function() {
let source = r#"<template><div>Hello</div></template>
<script setup>
const msg = 'world'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions {
is_production: true,
..Default::default()
};
let result = generate(source, &options, &allocator);
assert!(
result.code.contains("return (_ctx, _cache) =>"),
"Production should return inline render function. Generated:\n{}",
result.code
);
assert!(
!result.code.contains("function render("),
"Production should NOT have separate render export. Generated:\n{}",
result.code
);
assert!(
!result.code.contains("__returned__"),
"Production should NOT have __returned__ object. Generated:\n{}",
result.code
);
assert!(
!result.code.contains("return {msg}"),
"Production inline should NOT have 'return {{msg}}' — setup should return render fn. Generated:\n{}",
result.code
);
}
#[test]
fn test_production_keeps_expose_when_defineExpose_used() {
let source = r#"<template><div>{{ msg }}</div></template>
<script setup>
const msg = 'Hello'
defineExpose({ msg })
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions {
is_production: true,
..Default::default()
};
let result = generate(source, &options, &allocator);
assert!(
result.code.contains("expose: __expose") || result.code.contains("expose:__expose"),
"Production with defineExpose should have __expose in signature. Generated:\n{}",
result.code
);
assert!(
result.code.contains("__expose({ msg })"),
"Should have user's defineExpose call. Generated:\n{}",
result.code
);
}
#[test]
fn test_production_keeps_emit_when_defineEmits_with_declarator() {
let source = r#"<template><div @click="emit('click')">Click</div></template>
<script setup>
const emit = defineEmits(['click'])
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions {
is_production: true,
..Default::default()
};
let result = generate(source, &options, &allocator);
assert!(
result.code.contains("emit:__emit") || result.code.contains("emit: __emit"),
"Production with emit declarator should have __emit in signature. Generated:\n{}",
result.code
);
}
#[test]
fn test_development_mode_unchanged() {
let source = r#"<template><div>{{ msg }}</div></template>
<script setup>
const msg = 'Hello'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions {
is_production: false, ..Default::default()
};
let result = generate(source, &options, &allocator);
assert!(
result.code.contains("expose:__expose") || result.code.contains("expose: __expose"),
"Development should have __expose in signature. Generated:\n{}",
result.code
);
assert!(
result.code.contains("__expose()"),
"Development should call __expose(). Generated:\n{}",
result.code
);
assert!(
result.code.contains("__returned__"),
"Development should have __returned__ object. Generated:\n{}",
result.code
);
assert!(
result.code.contains("function render("),
"Development should have separate render function. Generated:\n{}",
result.code
);
}
#[test]
fn test_production_no_isScriptSetup_property() {
let source = r#"<template><div>Test</div></template>
<script setup>
const x = 1
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions {
is_production: true,
..Default::default()
};
let result = generate(source, &options, &allocator);
assert!(
!result.code.contains("__isScriptSetup"),
"Production should NOT have __isScriptSetup property. Generated:\n{}",
result.code
);
}
}
#[cfg(test)]
mod e2e_tests {
use super::*;
use oxc_parser::Parser;
use oxc_span::SourceType;
fn strip_source_map(code: &str) -> &str {
code.split("//# sourceMappingURL=")
.next()
.unwrap_or(code)
.trim_end()
}
fn assert_valid_js(code: &str, context: &str) {
let allocator = oxc_allocator::Allocator::default();
let source_type = SourceType::mjs();
let parser_result = Parser::new(&allocator, code, source_type).parse();
assert!(
parser_result.errors.is_empty(),
"Generated code is NOT valid JavaScript!\n\
Context: {}\n\
Parse Errors: {:?}\n\
Generated Code:\n{}",
context,
parser_result.errors,
code
);
}
#[allow(dead_code)]
fn gen_and_validate(source: &str) -> String {
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue".to_string());
let result = generate(source, &options, &allocator);
assert_valid_js(&result.code, source);
result.code
}
#[allow(dead_code)]
const INVALID_PATTERNS: &[(&str, &str)] = &[
("{ :", "empty property name (v-bind spread bug)"),
("_ctx.{", "object literal after _ctx. (class/style bug)"),
("_ctx.[", "array literal after _ctx. (class/style bug)"),
(
"{ v-",
"hyphenated directive as property (custom directive bug)",
),
(": _ctx.!", "negation in wrong position"),
("null))", "dangling null closing parens"),
(", ,", "double comma"),
(
"\"_toDisplayString",
"missing string concatenation operator",
),
("{ on:", "malformed v-once output"),
];
#[allow(dead_code)]
fn assert_no_invalid_patterns(code: &str, context: &str) {
for (pattern, desc) in INVALID_PATTERNS {
assert!(
!code.contains(pattern),
"Found invalid pattern '{}' ({}) in {}.\nGenerated:\n{}",
pattern,
desc,
context,
code
);
}
}
#[allow(dead_code)]
fn compare_ast_structure(our_code: &str, vue_code: &str, context: &str) -> Vec<String> {
let allocator1 = oxc_allocator::Allocator::default();
let allocator2 = oxc_allocator::Allocator::default();
let source_type = SourceType::mjs();
let our_result = Parser::new(&allocator1, our_code, source_type).parse();
let vue_result = Parser::new(&allocator2, vue_code, source_type).parse();
let mut diffs = Vec::new();
if !our_result.errors.is_empty() {
diffs.push(format!(
"[{}] Our code has parse errors: {:?}",
context, our_result.errors
));
return diffs;
}
if !vue_result.errors.is_empty() {
diffs.push(format!(
"[{}] Vue code has parse errors: {:?}",
context, vue_result.errors
));
return diffs;
}
let our_stmts = our_result.program.body.len();
let vue_stmts = vue_result.program.body.len();
if our_stmts != vue_stmts {
diffs.push(format!(
"[{}] Statement count differs: ours={}, vue={}",
context, our_stmts, vue_stmts
));
}
let our_imports: Vec<_> = our_result
.program
.body
.iter()
.filter_map(|s| {
if let oxc_ast::ast::Statement::ImportDeclaration(decl) = s {
Some(decl.source.value.as_str())
} else {
None
}
})
.collect();
let vue_imports: Vec<_> = vue_result
.program
.body
.iter()
.filter_map(|s| {
if let oxc_ast::ast::Statement::ImportDeclaration(decl) = s {
Some(decl.source.value.as_str())
} else {
None
}
})
.collect();
if our_imports != vue_imports {
diffs.push(format!(
"[{}] Import sources differ: ours={:?}, vue={:?}",
context, our_imports, vue_imports
));
}
diffs
}
#[test]
fn e2e_named_slot_with_default_opens_default_slot() {
let vue_source = r#"<script setup>
import { Dropdown } from "@nexus/ui"
</script>
<template>
<Dropdown>
<template #reference>
<span>Ref</span>
</template>
<div>Content</div>
</Dropdown>
</template>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "named slots + default render output");
assert!(
our_render.contains("reference: _withCtx"),
"Expected named slot to use withCtx. Output:\n{}",
our_render
);
assert!(
our_render.contains("default: _withCtx"),
"Expected default slot to be emitted when named slots exist. Output:\n{}",
our_render
);
assert!(
our_render.contains("]), _: 1 /* STABLE */"),
"Expected default slot array to be closed before stability marker. Output:\n{}",
our_render
);
}
#[test]
fn e2e_default_content_before_named_slot() {
let vue_source = r#"<template>
<MyComponent>
<div>Default content</div>
<template #configuration>
<span>Config</span>
</template>
</MyComponent>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "default content before named slot");
assert!(
our_render.contains("default: _withCtx"),
"Expected default slot. Output:\n{}",
our_render
);
assert!(
our_render.contains("configuration: _withCtx"),
"Expected configuration named slot. Output:\n{}",
our_render
);
assert!(
our_render.contains("]), configuration:"),
"Expected default slot to be closed before named slot with comma separator. Output:\n{}",
our_render
);
}
#[test]
fn e2e_render_imports_before_hoisted() {
let vue_source = r#"<template><div class=\"a\">Hi</div></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "render import order");
let import_pos = our_render.find("import ").unwrap_or(usize::MAX);
let hoisted_pos = our_render.find("const _hoisted_").unwrap_or(usize::MAX);
assert!(
import_pos < hoisted_pos,
"Expected imports before hoisted constants. Output:\n{}",
our_render
);
}
#[test]
fn e2e_render_uses_full_signature_with_bindings() {
let vue_source =
r#"<template><div>{{ msg }}</div></template><script setup>const msg = 'hi'</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "render signature");
assert!(
our_render
.contains("export function render(_ctx, _cache, $props, $setup, $data, $options)"),
"Expected full 6-arg render signature with export when bindings exist. Output:\n{}",
our_render
);
assert!(
our_render.contains("$setup.msg"),
"Expected $setup.msg for setup binding. Output:\n{}",
our_render
);
}
#[test]
fn e2e_render_uses_two_param_signature_without_bindings() {
let vue_source = r#"<template><div>Hello</div></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "render signature no bindings");
assert!(
our_render.contains("export function render(_ctx, _cache)"),
"Expected 2-arg render signature when no bindings. Output:\n{}",
our_render
);
}
#[test]
fn e2e_root_vif_velse_chain_renders_without_fragment_comma_breakage() {
let vue_source = r#"<template>
<a
v-if="isStringUrl && isExternalLink"
class="leading-none"
v-bind="$attrs"
:href="to"
rel="noopener noreferrer"
target="_blank"
>
<slot />
</a>
<RouterLink v-else v-slot="{ isActive, href, navigate }" v-bind="$props" custom>
<a
v-bind="$attrs"
class="leading-none"
:href="href"
:class="isActive ? activeClass : inactiveClass"
@click="navigate"
>
<slot />
</a>
</RouterLink>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "root v-if/v-else chain render output");
assert!(
!our_render.contains(",\n :"),
"render output must not emit a comma before ternary else branch. Output:\n{}",
our_render
);
assert!(
!our_render.contains("_createElementBlock(_Fragment"),
"root v-if/v-else pair should not be wrapped in a Fragment. Output:\n{}",
our_render
);
assert!(
our_render.contains("?") && our_render.contains(":"),
"root v-if/v-else should compile as ternary branches. Output:\n{}",
our_render
);
}
#[test]
fn e2e_simple_component() {
let source = r#"<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script setup>
const msg = 'Hello World'
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("simple.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("export default"),
"Should have __sfc__ component definition"
);
assert!(
code.contains("__name: 'simple'"),
"Should have component name derived from filename. Generated:\n{}",
code
);
assert!(code.contains("setup(__props"), "Should have setup function");
assert!(code.contains("__expose()"), "Should auto-expose");
assert!(
code.contains("const msg = 'Hello World'"),
"Should preserve script content"
);
assert!(
code.contains("__returned__={msg}"),
"Should return msg binding"
);
assert!(
code.contains("function render("),
"Should have render function"
);
assert!(
code.contains("_toDisplayString"),
"Should use toDisplayString for interpolation"
);
assert!(
code.contains("class: \"hello\""),
"Should preserve class attribute"
);
}
#[test]
fn e2e_no_script_component() {
let source = r#"<template>
<div>Hello world</div>
</template>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("no-script.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("function render("),
"Should have render function"
);
assert!(code.contains("\"Hello world\""), "Should have text content");
}
#[test]
fn e2e_conditional_v_if_v_else() {
let source = r#"<template>
<div>
<span v-if="show">Visible</span>
<span v-else>Hidden</span>
</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("conditional.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("$setup.show"),
"Should reference $setup.show in render, got:\n{}",
code
);
assert!(
code.contains("?") && code.contains(":"),
"Should use ternary operator for v-if/v-else, got:\n{}",
code
);
assert!(
code.contains("\"Visible\""),
"Should have v-if content, got:\n{}",
code
);
assert!(
code.contains("\"Hidden\""),
"Should have v-else content, got:\n{}",
code
);
assert!(
code.contains("export default"),
"Should have __sfc__ component, got:\n{}",
code
);
assert!(
code.contains("__returned__={show}"),
"Should return show binding, got:\n{}",
code
);
}
#[test]
fn e2e_vif_no_else_then_slot_in_component() {
let vue_source = r#"<template>
<MyComponent>
<template v-if="show">
<span>Visible</span>
</template>
<slot />
<template v-if="other">
<span>Other</span>
</template>
</MyComponent>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "v-if no else then slot in component");
assert!(
our_render.contains("_createCommentVNode"),
"Expected createCommentVNode for v-if without v-else. Output:\n{}",
our_render
);
assert!(
our_render.contains("_renderSlot"),
"Expected renderSlot for <slot />. Output:\n{}",
our_render
);
}
#[test]
fn e2e_html_comment_inside_component() {
let vue_source = r#"<template>
<MyComponent v-model="drawer">
<!-- -->
</MyComponent>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "html comment inside component");
assert!(
our_render.contains("default: _withCtx"),
"Expected comment to be in default slot. Output:\n{}",
our_render
);
assert!(
our_render.contains("_createCommentVNode"),
"Expected createCommentVNode for HTML comment. Output:\n{}",
our_render
);
}
#[test]
fn e2e_component_with_br_children_and_nested_components() {
let vue_source = r#"<template>
<VSheet>
<VResponsive>
<span>Inner</span>
</VResponsive>
<br>
<br>
<SponsorLink size="large" />
</VSheet>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(
&our_render,
"component with br children and nested components",
);
}
#[test]
fn e2e_component_with_named_slot_then_default_content() {
let vue_source = r#"<template>
<VTooltip location="bottom">
<template #activator="{ props }">
<a v-bind="props">Link</a>
</template>
Opens in new window
</VTooltip>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(
&our_render,
"component with named slot then default content",
);
}
#[test]
fn e2e_nested_component_named_slot_then_default_in_vslot() {
let vue_source = r#"<template>
<VCheckbox v-model="checkbox">
<template #label>
<div>
I agree that
<VTooltip location="bottom">
<template #activator="{ props }">
<a v-bind="props">Vuetify</a>
</template>
Opens in new window
</VTooltip>
is awesome
</div>
</template>
</VCheckbox>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(
&our_render,
"nested component named slot then default in vslot",
);
}
#[test]
fn e2e_interpolation_inside_component_in_vslot() {
let vue_source = r#"<template>
<I18nT keypath="ready-text" scope="global" tag="div">
<template #team>
<AppLink :href="url">
{{ t('team') }}
</AppLink>
</template>
</I18nT>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "interpolation inside component in vslot");
assert!(
our_render.contains("_createTextVNode"),
"Expected _createTextVNode for interpolation in component slot. Output:\n{}",
our_render
);
}
#[test]
fn e2e_comment_inside_named_slot() {
let vue_source = r#"<template>
<VBanner text="hello">
<template #prepend>
<!-- rounded added due to bug -->
<VAvatar icon="$vuetify" class="text-white" rounded="circle" />
</template>
</VBanner>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "comment inside named slot");
assert!(
our_render.contains("_createCommentVNode"),
"Expected _createCommentVNode. Output:\n{}",
our_render
);
}
#[test]
fn e2e_conditional_named_slot_simple() {
let vue_source = r#"<template>
<VBanner text="hello">
<template v-if="showPrepend" #prepend>
<VIcon icon="mdi-check" />
</template>
</VBanner>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "conditional named slot simple");
assert!(
our_render.contains("_createSlots"),
"Expected _createSlots for conditional named slot. Output:\n{}",
our_render
);
assert!(
our_render.contains("DYNAMIC"),
"Expected DYNAMIC slot flag. Output:\n{}",
our_render
);
}
#[test]
fn e2e_conditional_named_slot_with_else() {
let vue_source = r#"<template>
<VBanner text="hello">
<template v-if="showPrepend" #prepend>
<VIcon icon="mdi-check" />
</template>
<template v-else #prepend>
<VIcon icon="mdi-alert" />
</template>
</VBanner>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "conditional named slot with else");
assert!(
our_render.contains("_createSlots"),
"Expected _createSlots. Output:\n{}",
our_render
);
}
#[test]
fn e2e_conditional_and_static_named_slots() {
let vue_source = r#"<template>
<VBanner text="hello">
<template v-if="showPrepend" #prepend>
<VIcon icon="mdi-check" />
</template>
<template #actions>
<VBtn>OK</VBtn>
</template>
</VBanner>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "conditional and static named slots");
assert!(
our_render.contains("_createSlots"),
"Expected _createSlots. Output:\n{}",
our_render
);
}
#[test]
fn e2e_multiple_conditional_named_slots() {
let vue_source = r#"<template>
<VBanner text="hello">
<template v-if="showPrepend" #prepend>
<VIcon icon="mdi-check" />
</template>
<template v-if="showAppend" #append>
<VIcon icon="mdi-close" />
</template>
</VBanner>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "multiple conditional named slots");
assert!(
our_render.contains("_createSlots"),
"Expected _createSlots. Output:\n{}",
our_render
);
}
#[test]
fn e2e_hyphenated_slot_name() {
let vue_source = r#"<template>
<VSelect :items="items">
<template #append-outer>
<div>suffix</div>
</template>
</VSelect>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "hyphenated slot name");
assert!(
our_render.contains("\"append-outer\""),
"Expected quoted \"append-outer\" slot name. Output:\n{}",
our_render
);
}
#[test]
fn e2e_hyphenated_slot_name_scoped() {
let vue_source = r#"<template>
<VSelect :items="items">
<template #prepend-item="{ item }">
<span>{{ item }}</span>
</template>
</VSelect>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "hyphenated scoped slot name");
assert!(
our_render.contains("\"prepend-item\""),
"Expected quoted \"prepend-item\" slot name. Output:\n{}",
our_render
);
}
#[test]
fn e2e_vif_inside_named_slot() {
let vue_source = r#"<template>
<VListItem>
<template #subtitle>
<span v-if="show">{{ text }}</span>
</template>
</VListItem>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "v-if inside named slot");
assert!(
our_render.contains("_createCommentVNode"),
"Expected _createCommentVNode for v-if fallback. Output:\n{}",
our_render
);
}
#[test]
fn e2e_default_slot_with_conditional_named_slot() {
let vue_source = r#"<template>
<VListItem lines="two">
<template v-if="showPrepend" #prepend>
<VAvatar image="test.png" />
</template>
<VListItemTitle>Title</VListItemTitle>
</VListItem>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "default slot with conditional named slot");
assert!(
our_render.contains("_createSlots"),
"Expected _createSlots. Output:\n{}",
our_render
);
}
#[test]
fn e2e_object_literal_keys_not_prefixed() {
let vue_source = r#"<template>
<span>{{ t('hello', { count: items.length }) }}</span>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "object literal keys not prefixed");
assert!(
!our_render.contains("_ctx.count"),
"Object key 'count' should not be prefixed with _ctx. Output:\n{}",
our_render
);
}
#[test]
fn e2e_text_and_interpolation_in_slot() {
let vue_source = r#"<template>
<VVirtualScroll :items="items" height="200">
<template v-slot:default="{ item }">
Virtual Item {{ item }}
</template>
</VVirtualScroll>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "text and interpolation in slot");
}
#[test]
fn e2e_vif_component_with_named_slot() {
let vue_source = r#"<template>
<div>
<MyChip v-if="show" text="hello">
<template #prepend>
<MyIcon color="purple" />
</template>
</MyChip>
</div>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "v-if component with named slot");
}
#[test]
fn e2e_vif_velse_inside_named_slot() {
let vue_source = r#"<template>
<div>
<MyTooltip v-if="show">
<template #activator="{ props }">
<a v-if="link" :href="link">Link</a>
<div v-else>Fallback</div>
</template>
</MyTooltip>
</div>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "v-if/v-else inside named slot");
}
#[test]
fn e2e_shorthand_property_with_prefix() {
let vue_source = r#"<template>
<span>{{ t('missing', { file }) }}</span>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "shorthand property with prefix");
}
#[test]
fn e2e_shorthand_property_with_script_setup() {
let vue_source = r#"<script setup>
const props = defineProps({ file: String })
const { t } = useI18n()
</script>
<template>
<MyComp v-text="t('missing', { file })" />
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "shorthand property with script setup");
assert!(
!our_render.contains("{ __props.file }"),
"Should expand shorthand to key: value format. Generated:\n{}",
our_render
);
}
#[test]
fn e2e_template_literal_not_treated_as_shorthand() {
let vue_source = r#"<script setup>
const branch = ref('main')
</script>
<template>
<a :href="`https://github.com/tree/${branch}/src`">Link</a>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "template literal not treated as shorthand");
assert!(
!our_render.contains("${branch:"),
"Template literal ${{}}branch{{}} should NOT be treated as shorthand property. Generated:\n{}",
our_render
);
}
#[test]
fn e2e_text_interpolation_then_element_in_slot() {
let vue_source = r#"<template>
<MyFooter>
<template #default>
{{ year }} --- <strong>Vuetify</strong>
</template>
</MyFooter>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "text interpolation then element in slot");
}
#[test]
fn e2e_compound_event_name_with_colon() {
let vue_source = r#"<template>
<VChip @click="select" @click:close="remove(item)" v-bind="attrs">Hello</VChip>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "compound event name with colon");
assert!(
our_render.contains("\"onClick:close\""),
"Compound event name onClick:close should be quoted. Generated:\n{}",
our_render
);
}
#[test]
fn e2e_vif_template_fragment_with_velse_sibling() {
let vue_source = r#"<template>
<MyParent>
<template v-if="!error1 && !error2">
<MyBase>
<Inner v-if="!error1" />
<Inner v-if="error1" />
</MyBase>
</template>
<Fallback v-else />
</MyParent>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "v-if template fragment with v-else sibling");
}
#[test]
fn e2e_event_modifier_no_handler() {
let vue_source = r#"<template>
<a href="https://example.com" @click.stop>Link</a>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "event modifier no handler");
}
#[test]
fn e2e_list_v_for() {
let source = r#"<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
])
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("list.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("_renderList"),
"Should use _renderList for v-for, got:\n{}",
code
);
assert!(
code.contains("$setup.items"),
"Should reference $setup.items, got:\n{}",
code
);
assert!(
code.contains("item.id") || code.contains("key:"),
"Should handle :key binding, got:\n{}",
code
);
assert!(
code.contains("export default"),
"Should have __sfc__ component, got:\n{}",
code
);
assert!(
code.contains("__returned__={items}"),
"Should return items binding, got:\n{}",
code
);
}
#[test]
fn e2e_unicode_content() {
let source = r#"<template>
<div>😊 Unicode Test 😊</div>
<div>😊 Unicode Test 😊</div>
<div>😊 Unicode Test 😊</div>
</template>
<script setup>
import { ref } from 'vue'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("unicode.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("😊") || code.contains("Unicode Test"),
"Should preserve unicode content"
);
assert!(
code.contains("function render("),
"Should have render function"
);
}
#[test]
fn e2e_async_setup() {
let source = r#"<template>
<div>Hello world</div>
</template>
<script setup>
import { ref } from "vue";
const foo = ref("");
await Promise.resolve();
async () => {
await Promise.resolve();
};
let a = {};
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("simple.async.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("_withAsyncContext"),
"Should wrap top-level await with _withAsyncContext"
);
assert!(
code.contains("__temp") && code.contains("__restore"),
"Should have __temp and __restore for async context"
);
assert!(
code.contains("async ()"),
"Should preserve nested async function"
);
}
#[test]
fn e2e_slot_outlet() {
let source = r#"<template>
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
</template>
<script setup>
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("slot-outlet.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("_renderSlot"),
"Should use _renderSlot for slot outlets"
);
assert!(code.contains("\"header\""), "Should have header slot name");
assert!(code.contains("\"default\""), "Should have default slot");
assert!(code.contains("\"footer\""), "Should have footer slot name");
assert!(code.contains("_ctx.$slots"), "Should reference _ctx.$slots");
}
#[test]
fn e2e_scoped_slots() {
let source = r#"<template>
<MyComponent>
<template #header="{ title }">
{{ title }}
</template>
</MyComponent>
</template>
<script setup>
import MyComponent from './MyComponent.vue'
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("slots.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("_withCtx"),
"Should use _withCtx for scoped slots"
);
assert!(code.contains("header:"), "Should have header slot property");
assert!(
code.contains("{ title }")
|| code.contains("({title})")
|| code.contains("({ title })"),
"Should destructure slot props"
);
assert!(
code.contains("_toDisplayString"),
"Should use _toDisplayString for slot content"
);
}
#[test]
fn e2e_slot_self_closing_default() {
let source = r#"<template>
<slot/>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
eprintln!("=== slot self-closing default ===\n{}\n=== END ===", code);
assert!(
code.contains("_renderSlot"),
"Should use _renderSlot. Generated:\n{}",
code
);
assert!(
code.contains("\"default\""),
"Should have 'default' slot name. Generated:\n{}",
code
);
assert_valid_js(&code, "self-closing default slot");
}
#[test]
fn e2e_slot_self_closing_named() {
let source = r#"<template>
<slot name="second" />
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
eprintln!("=== slot self-closing named ===\n{}\n=== END ===", code);
assert!(
code.contains("_renderSlot"),
"Should use _renderSlot. Generated:\n{}",
code
);
assert!(
code.contains("\"second\""),
"Should have 'second' slot name. Generated:\n{}",
code
);
assert_valid_js(&code, "self-closing named slot");
}
#[test]
fn e2e_unicode_before_template() {
let source = r#"<!-- 红红红红红 table -->
<template>
<div v-if="barfoo.key === 'bzz'">{{ MyPotion }}</div>
</template>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("unicode-test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("function render("),
"Should have render function. Generated:\n{}",
code
);
assert!(
code.contains("_ctx.barfoo"),
"Should reference barfoo through _ctx. Generated:\n{}",
code
);
}
#[test]
fn e2e_define_props_typed() {
let source = r#"<template>
<div>{{ title }}</div>
</template>
<script setup lang="ts">
defineProps<{ title: string }>()
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("props.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(code.contains("props:"), "Should have props definition");
assert!(code.contains("title"), "Should have title prop");
assert!(
code.contains("_defineComponent"),
"TypeScript should use _defineComponent"
);
}
#[test]
fn e2e_define_emits_typed() {
let source = r#"<template>
<button @click="emit('click')">Click</button>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(e: 'click'): void
}>()
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("emits.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(code.contains("emits:"), "Should have emits definition");
assert!(
code.contains("emit:__emit"),
"Should expose emit in setup context"
);
assert!(
code.contains("const emit = __emit"),
"Should assign __emit to emit"
);
}
#[test]
fn e2e_define_model() {
let source = r#"<template>
<input v-model="model" />
</template>
<script setup lang="ts">
const model = defineModel<string>()
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("model.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(code.contains("_useModel"), "Should use _useModel helper");
assert!(code.contains("modelValue"), "Should have modelValue prop");
assert!(
code.contains("update:modelValue"),
"Should have update:modelValue emit"
);
assert!(
code.contains("modelValueModifiers"),
"Should have modelValueModifiers prop"
);
}
#[test]
fn e2e_define_expose() {
let source = r#"<template>
<div>Test</div>
</template>
<script setup lang="ts">
const publicMethod = () => 'hello'
defineExpose({ publicMethod })
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("expose.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("__expose({ publicMethod })"),
"Should replace defineExpose with __expose"
);
assert!(
!code.contains("__expose();"),
"Should not auto-expose when user has defineExpose"
);
}
#[test]
fn e2e_define_slots() {
let source = r#"<template>
<div><slot /></div>
</template>
<script setup lang="ts">
const slots = defineSlots<{
default: () => any
}>()
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("define-slots.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("export default") || code.contains("_defineComponent"),
"Should have component definition, got:\n{}",
code
);
assert!(
code.contains("_renderSlot") || code.contains("slot"),
"Should render slot, got:\n{}",
code
);
}
#[test]
fn e2e_source_map_valid_json() {
let source = r#"<template><div>{{ msg }}</div></template>
<script setup>
const msg = 'test'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new()
.with_filename("test.vue")
.include_source_content(true);
let result = generate(source, &options, &allocator);
let map: serde_json::Value =
serde_json::from_str(&result.source_map).expect("Source map should be valid JSON");
assert_eq!(map["version"], 3, "Should be version 3 source map");
assert!(
map["sources"].as_array().unwrap().len() > 0,
"Should have sources"
);
assert!(map["mappings"].as_str().is_some(), "Should have mappings");
}
#[test]
fn e2e_source_map_includes_content() {
let source = r#"<template><div>Hello</div></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new()
.with_filename("test.vue")
.include_source_content(true);
let result = generate(source, &options, &allocator);
let map: serde_json::Value =
serde_json::from_str(&result.source_map).expect("Source map should be valid JSON");
assert!(
map["sourcesContent"].as_array().is_some(),
"Should include sourcesContent when requested"
);
}
#[test]
fn e2e_imports_hoisted() {
let source = r#"<template>
<div>{{ count }}</div>
</template>
<script setup>
import { ref } from 'vue'
import { computed } from 'vue'
const count = ref(0)
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("imports.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("import {") || code.contains("import{"),
"Should have imports, got:\n{}",
code
);
assert!(
code.contains("from \"vue\"") || code.contains("from 'vue'"),
"Should import from vue, got:\n{}",
code
);
assert!(
code.contains("export default"),
"Should have __sfc__, got:\n{}",
code
);
}
#[test]
fn e2e_empty_template() {
let source = r#"<template></template>
<script setup>
const x = 1
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("empty.vue");
let result = generate(source, &options, &allocator);
assert!(!result.code.is_empty(), "Should produce output");
}
#[test]
fn e2e_nested_elements() {
let source = r#"<template>
<div>
<section>
<article>
<p>Deep nesting</p>
</article>
</section>
</div>
</template>
<script setup></script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("nested.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(code.contains("div"), "Should have div");
assert!(code.contains("section"), "Should have section");
assert!(code.contains("article"), "Should have article");
assert!(
code.contains("\"Deep nesting\""),
"Should have text content"
);
}
#[test]
fn e2e_multiple_root_elements() {
let source = r#"<template>
<div>First</div>
<div>Second</div>
<div>Third</div>
</template>
<script setup></script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("multi-root.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(code.contains("\"First\""), "Should have First");
assert!(code.contains("\"Second\""), "Should have Second");
assert!(code.contains("\"Third\""), "Should have Third");
}
#[test]
fn e2e_multiple_root_elements_fragment_wrapper() {
let source = r#"<template>
<div>First</div>
<div>Second</div>
<div>Third</div>
</template>
<script setup></script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("multi-root.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("_Fragment"),
"Multiple root elements should be wrapped in Fragment. Generated:\n{}",
code
);
assert!(
code.contains("_createElementBlock(_Fragment"),
"Should use _createElementBlock with _Fragment. Generated:\n{}",
code
);
assert!(
code.contains("64") || code.contains("STABLE_FRAGMENT"),
"Should have STABLE_FRAGMENT patch flag. Generated:\n{}",
code
);
}
#[test]
fn e2e_single_root_no_fragment() {
let source = r#"<template>
<div>Single root</div>
</template>
<script setup></script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("single-root.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
!code.contains("_Fragment"),
"Single root element should NOT use Fragment. Generated:\n{}",
code
);
}
#[test]
fn e2e_component_name_from_filename() {
let source = r#"<template>
<div>Test</div>
</template>
<script setup></script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("my-component.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("__name: 'my-component'"),
"Component name should be 'my-component' (from filename). Generated:\n{}",
code
);
assert!(
!code.contains("__name: 'App'"),
"Component name should NOT be hardcoded 'App'. Generated:\n{}",
code
);
}
#[test]
fn e2e_component_name_from_path() {
let source = r#"<template>
<div>Test</div>
</template>
<script setup></script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("src/components/MyComponent.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("__name: 'MyComponent'"),
"Component name should be 'MyComponent' (from path). Generated:\n{}",
code
);
}
#[test]
fn e2e_patch_flag_text_for_interpolation() {
let source = r#"<template>
<div>
<span>{{ message }}</span>
</div>
</template>
<script setup>
const message = 'Hello'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("1 /* TEXT */") || code.contains(", 1)"),
"Element with interpolation should have TEXT patch flag. Generated:\n{}",
code
);
}
#[test]
fn e2e_patch_flag_class_for_dynamic_class() {
let source = r#"<template>
<div :class="dynamicClass">Content</div>
</template>
<script setup>
const dynamicClass = 'active'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("2 /* CLASS */") || code.contains(", 2)"),
"Element with dynamic class should have CLASS patch flag. Generated:\n{}",
code
);
}
#[test]
fn e2e_vif_velse_has_key_props() {
let source = r#"<template>
<div>
<span v-if="show">Visible</span>
<span v-else>Hidden</span>
</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("key: 0") || code.contains("{ key: 0 }"),
"v-if branch should have key: 0. Generated:\n{}",
code
);
assert!(
code.contains("key: 1") || code.contains("{ key: 1 }"),
"v-else branch should have key: 1. Generated:\n{}",
code
);
}
#[test]
fn e2e_self_closing_tags() {
let source = r#"<template>
<input type="text" />
<br />
<img src="test.png" />
</template>
<script setup></script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("self-closing.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(code.contains("input"), "Should have input");
assert!(code.contains("br"), "Should have br");
assert!(code.contains("img"), "Should have img");
}
#[test]
fn e2e_script_setup_declarations_preserved() {
let source = r#"<template>
<div>{{ show }}</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("const show = ref(true)"),
"Declaration should be preserved in setup function. Generated:\n{}",
code
);
}
#[test]
fn e2e_script_setup_with_conditional_template() {
let source = r#"<template>
<div>
<span v-if="show">Visible</span>
<span v-else>Hidden</span>
</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("conditional.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("const show = ref(true)"),
"v-if/v-else template should not delete script declarations. Generated:\n{}",
code
);
}
#[test]
fn e2e_template_overwrite_stays_within_bounds() {
let source = r#"<template>
<div>content</div>
</template>
<script setup>
const foo = 'bar'
</script>
"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("const foo = 'bar'") || code.contains("const foo='bar'"),
"Script declarations should not be overwritten by template processing. Generated:\n{}",
code
);
}
#[test]
fn e2e_single_text_child_no_array() {
let source = r#"<template>
<span>Hello</span>
</template>
<script setup></script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
!code.contains(r#"["Hello"]"#),
"Single text child should NOT be wrapped in array. Generated:\n{}",
code
);
assert!(
code.contains(r#""span", null, "Hello""#) || code.contains(r#""span",null,"Hello""#),
"Single text child should be passed directly as string. Generated:\n{}",
code
);
}
#[test]
fn e2e_single_interpolation_child_no_array() {
let source = r#"<template>
<span>{{ msg }}</span>
</template>
<script setup>
const msg = 'Hello'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
!code.contains("[_toDisplayString"),
"Single interpolation child should NOT be wrapped in array. Generated:\n{}",
code
);
}
#[test]
fn e2e_multiple_children_use_array() {
let source = r#"<template>
<div>
<span>First</span>
<span>Second</span>
</div>
</template>
<script setup></script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("[") && code.contains("]"),
"Multiple children should use array format. Generated:\n{}",
code
);
}
#[test]
fn e2e_child_elements_no_openblock() {
let source = r#"<template>
<div class="parent">
<span>Child 1</span>
<span>Child 2</span>
</div>
</template>
<script setup></script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
let openblock_count = code.matches("_openBlock()").count();
assert_eq!(
openblock_count, 1,
"Should only have 1 _openBlock() for root element, found {}. Generated:\n{}",
openblock_count, code
);
assert!(
code.contains("_createElementVNode"),
"Child elements should use _createElementVNode. Generated:\n{}",
code
);
}
#[test]
fn e2e_vif_branches_have_openblock() {
let source = r#"<template>
<div>
<span v-if="show">Visible</span>
<span v-else>Hidden</span>
</div>
</template>
<script setup>
const show = true
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
let openblock_count = code.matches("_openBlock()").count();
assert!(
openblock_count >= 3,
"v-if branches should have _openBlock(). Found {} occurrences. Generated:\n{}",
openblock_count,
code
);
}
#[test]
fn e2e_keyed_vfor_items_are_block_root() {
let source = r#"<template>
<div>
<span v-for="item in items" :key="item">{{ item }}</span>
</div>
</template>
<script setup>
const items = ['a', 'b', 'c']
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains(r#"_createElementBlock("span""#),
"Keyed v-for items should use _createElementBlock. Generated:\n{}",
code
);
}
#[test]
fn e2e_event_modifier_stop_prevent() {
let source = r#"<template>
<button @click.stop.prevent="handleClick">Click</button>
</template>
<script setup>
const handleClick = () => {}
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("_withModifiers"),
"Should use _withModifiers for event modifiers. Generated:\n{}",
code
);
assert!(
code.contains("withModifiers as _withModifiers"),
"Should import withModifiers from vue. Generated:\n{}",
code
);
assert!(
code.contains(r#"["stop"#) && code.contains(r#""prevent"]"#),
"Should include stop and prevent in modifiers array. Generated:\n{}",
code
);
}
#[test]
fn e2e_event_modifier_capture_once() {
let source = r#"<template>
<button @click.capture="handleCapture">Capture</button>
<button @click.once="handleOnce">Once</button>
</template>
<script setup>
const handleCapture = () => {}
const handleOnce = () => {}
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("onClickCapture"),
".capture should become onClickCapture. Generated:\n{}",
code
);
assert!(
code.contains("onClickOnce"),
".once should become onClickOnce. Generated:\n{}",
code
);
}
#[test]
fn e2e_event_handler_with_colon_in_name() {
let vue_source = r#"<template>
<MyComponent @update:rail="onUpdateRail" />
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "event handler with colon in name");
assert!(
our_render.contains("\"onUpdate:rail\""),
"Expected quoted event name for onUpdate:rail. Output:\n{}",
our_render
);
}
#[test]
fn e2e_key_modifier_enter() {
let source = r#"<template>
<input @keyup.enter="handleEnter" />
</template>
<script setup>
const handleEnter = () => {}
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("_withKeys"),
"Should use _withKeys for key modifiers. Generated:\n{}",
code
);
assert!(
code.contains("withKeys as _withKeys"),
"Should import withKeys from vue. Generated:\n{}",
code
);
assert!(
code.contains(r#"["enter"]"#),
"Should include 'enter' in keys array. Generated:\n{}",
code
);
}
#[test]
fn e2e_key_modifier_with_system_modifier() {
let source = r#"<template>
<input @keyup.ctrl.enter="handleCtrlEnter" />
</template>
<script setup>
const handleCtrlEnter = () => {}
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("_withKeys") && code.contains("_withModifiers"),
"Should use both _withKeys and _withModifiers. Generated:\n{}",
code
);
assert!(
code.contains(r#"["ctrl"]"#),
"Should include 'ctrl' in modifiers array. Generated:\n{}",
code
);
assert!(
code.contains(r#"["enter"]"#),
"Should include 'enter' in keys array. Generated:\n{}",
code
);
}
#[test]
fn e2e_dynamic_component_basic() {
let source = r#"<template>
<component :is="currentComponent" />
</template>
<script setup>
import { ref } from 'vue'
const currentComponent = ref('MyComponent')
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("_resolveDynamicComponent"),
"Should use _resolveDynamicComponent for <component :is>. Generated:\n{}",
code
);
assert!(
code.contains("resolveDynamicComponent as _resolveDynamicComponent"),
"Should import resolveDynamicComponent from vue. Generated:\n{}",
code
);
assert!(
code.contains("$setup.currentComponent"),
"Should reference the :is binding. Generated:\n{}",
code
);
}
#[test]
fn e2e_dynamic_component_with_props() {
let source = r#"<template>
<component :is="currentView" :title="pageTitle" @click="handleClick" />
</template>
<script setup>
import { ref } from 'vue'
const currentView = ref('Home')
const pageTitle = ref('Welcome')
const handleClick = () => {}
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue");
let result = generate(source, &options, &allocator);
let code = strip_source_map(&result.code_with_source_map);
assert!(
code.contains("_resolveDynamicComponent"),
"Should use _resolveDynamicComponent. Generated:\n{}",
code
);
assert!(
code.contains("title:"),
"Should include title prop. Generated:\n{}",
code
);
assert!(
code.contains("onClick"),
"Should include click handler. Generated:\n{}",
code
);
}
#[test]
fn e2e_simple_template_ast_is_valid_js() {
let vue_source = r#"<template>
<div class="hello">Hello</div>
</template>
<script setup>
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue".to_string());
let result = generate(vue_source, &options, &allocator);
assert_valid_js(&result.code, "simple template");
}
#[test]
fn e2e_simple_component_with_msg_ast_is_valid_js() {
let vue_source = r#"<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script setup>
const msg = 'Hello World'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue".to_string());
let result = generate(vue_source, &options, &allocator);
assert_valid_js(&result.code, "component with interpolation");
}
#[test]
fn e2e_vbind_spread_ast_is_valid_js() {
let vue_source = r#"<template>
<div v-bind="attrs">Content</div>
</template>
<script setup>
const attrs = { id: 'test' }
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue".to_string());
let result = generate(vue_source, &options, &allocator);
assert_valid_js(&result.code, "v-bind spread");
assert_no_invalid_patterns(&result.code, "v-bind spread");
}
#[test]
fn e2e_custom_directive_ast_is_valid_js() {
let vue_source = r#"<template>
<input v-focus />
</template>
<script setup>
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue".to_string());
let result = generate(vue_source, &options, &allocator);
assert_valid_js(&result.code, "custom directive");
assert_no_invalid_patterns(&result.code, "custom directive");
}
#[test]
fn e2e_vonce_ast_is_valid_js() {
let vue_source = r#"<template>
<span v-once>{{ staticContent }}</span>
</template>
<script setup>
const staticContent = 'Static'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue".to_string());
let result = generate(vue_source, &options, &allocator);
assert_valid_js(&result.code, "v-once");
assert_no_invalid_patterns(&result.code, "v-once");
}
#[test]
fn e2e_object_class_ast_is_valid_js() {
let vue_source = r#"<template>
<div :class="{ active: isActive }">Content</div>
</template>
<script setup>
const isActive = true
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue".to_string());
let result = generate(vue_source, &options, &allocator);
assert_valid_js(&result.code, "object class binding");
assert_no_invalid_patterns(&result.code, "object class binding");
}
#[test]
fn e2e_hyphenated_props_ast_is_valid_js() {
let vue_source = r#"<template>
<div :data-value="value">Content</div>
</template>
<script setup>
const value = 'test'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue".to_string());
let result = generate(vue_source, &options, &allocator);
assert_valid_js(&result.code, "hyphenated props");
}
#[test]
fn e2e_mixed_text_interpolation_ast_is_valid_js() {
let vue_source = r#"<template>
<span>Static: {{ content }}</span>
</template>
<script setup>
const content = 'dynamic'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue".to_string());
let result = generate(vue_source, &options, &allocator);
assert_valid_js(&result.code, "mixed text and interpolation");
}
#[test]
fn e2e_inline_object_literal_ast_is_valid_js() {
let vue_source = r#"<template>
<input :value="{ type: 'text', name: 'field' }" />
</template>
<script setup>
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("test.vue".to_string());
let result = generate(vue_source, &options, &allocator);
assert_valid_js(&result.code, "inline object literal");
}
#[test]
fn test_vif_without_velse_produces_valid_js() {
let vue_source = r#"<template>
<div>
<div v-if="show">
<span>Content</span>
</div>
</div>
</template>
<script setup>
const show = true
</script>"#;
let code = gen_and_validate(vue_source);
assert!(
!code.contains("[: _createCommentVNode"),
"Comment vnode should not be inside children array. Generated:\n{}",
code
);
assert!(
code.contains("_createCommentVNode(\"v-if\", true)"),
"Should have v-if comment vnode fallback. Generated:\n{}",
code
);
assert_no_invalid_patterns(&code, "v-if without v-else");
}
#[test]
fn test_vif_with_velse_produces_valid_js() {
let vue_source = r#"<template>
<div>
<div v-if="show">If content</div>
<div v-else>Else content</div>
</div>
</template>
<script setup>
const show = true
</script>"#;
let code = gen_and_validate(vue_source);
assert!(
!code.contains("_createCommentVNode(\"v-if\", true)"),
"Should NOT have comment vnode when v-else exists. Generated:\n{}",
code
);
assert_no_invalid_patterns(&code, "v-if with v-else");
}
#[test]
fn test_dynamic_component_with_static_is_produces_valid_js() {
let vue_source = r#"<template>
<div>
<component is="div">Static content</component>
</div>
</template>
<script setup>
</script>"#;
let code = gen_and_validate(vue_source);
assert!(
code.contains("_resolveDynamicComponent(\"div\")"),
"Static is value should be passed directly to _resolveDynamicComponent. Generated:\n{}",
code
);
assert!(
!code.contains("{ is:") && !code.contains("is: \"div\""),
"Static is should not appear as a prop. Generated:\n{}",
code
);
assert_no_invalid_patterns(&code, "dynamic component with static is");
}
#[test]
fn test_dynamic_component_with_children_produces_valid_js() {
let vue_source = r#"<template>
<div>
<component :is="comp">Child content</component>
</div>
</template>
<script setup>
const comp = 'div'
</script>"#;
let code = gen_and_validate(vue_source);
assert!(
code.contains("_withCtx"),
"Children should be wrapped with _withCtx for slots. Generated:\n{}",
code
);
assert_no_invalid_patterns(&code, "dynamic component with children");
}
#[test]
fn test_custom_directive_basic_produces_valid_js() {
let vue_source = r#"<template>
<div>
<input v-focus />
</div>
</template>
<script setup>
</script>"#;
let code = gen_and_validate(vue_source);
assert!(
code.contains("_withDirectives"),
"Custom directive should use _withDirectives. Generated:\n{}",
code
);
assert!(
code.contains("_resolveDirective(\"focus\")"),
"Custom directive should be resolved. Generated:\n{}",
code
);
assert!(
!code.contains("\"v-focus\""),
"Custom directive should not be treated as static prop. Generated:\n{}",
code
);
assert_no_invalid_patterns(&code, "custom directive basic");
}
#[test]
fn test_custom_directive_with_value_produces_valid_js() {
let vue_source = r#"<template>
<div>
<div v-tooltip="'Hello World'">Hover me</div>
</div>
</template>
<script setup>
</script>"#;
let code = gen_and_validate(vue_source);
assert!(
code.contains("_withDirectives"),
"Custom directive should use _withDirectives. Generated:\n{}",
code
);
assert!(
code.contains("_resolveDirective(\"tooltip\")"),
"Custom directive should be resolved. Generated:\n{}",
code
);
assert!(
code.contains("'Hello World'"),
"Custom directive should include value. Generated:\n{}",
code
);
assert_no_invalid_patterns(&code, "custom directive with value");
}
#[test]
fn test_vonce_basic_produces_valid_js() {
let vue_source = r#"<template>
<div>
<span v-once>Static: {{ content }}</span>
</div>
</template>
<script setup>
const content = 'test'
</script>"#;
let code = gen_and_validate(vue_source);
assert!(
code.contains("_setBlockTracking"),
"v-once should use _setBlockTracking. Generated:\n{}",
code
);
assert!(
code.contains("_cache[") && code.contains("] || ("),
"v-once should use cache pattern. Generated:\n{}",
code
);
assert!(
code.contains(".cacheIndex"),
"v-once should set cacheIndex. Generated:\n{}",
code
);
assert_no_invalid_patterns(&code, "v-once basic");
}
#[test]
fn test_scoped_style_adds_data_v_attribute() {
let source = r#"<template>
<div class="container">Hello</div>
</template>
<script setup></script>
<style scoped>.container { color: red; }</style>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("scoped-test.vue");
let result = generate(source, &options, &allocator);
assert!(
result.code.contains("data-v-"),
"Should have data-v attribute. Generated:\n{}",
result.code
);
assert!(
result.code.contains("__css__"),
"Should export __css__. Generated:\n{}",
result.code
);
assert!(
result.code.contains(".container[data-v-"),
"CSS should have scoped selector. Generated:\n{}",
result.code
);
}
#[test]
fn test_css_modules_exports_mapping() {
let source = r#"<template>
<div :class="$style.container">Hello</div>
</template>
<script setup></script>
<style module>.container { color: red; } .title { font-size: 16px; }</style>"#;
let allocator = oxc_allocator::Allocator::new();
let options = CodegenOptions::new().with_filename("module-test.vue");
let result = generate(source, &options, &allocator);
assert!(
result.code.contains("__css__"),
"Should export __css__. Generated:\n{}",
result.code
);
assert!(
result.code.contains("__cssModules__"),
"Should export __cssModules__. Generated:\n{}",
result.code
);
assert!(
result.code.contains("\"$style\""),
"Should have $style module. Generated:\n{}",
result.code
);
assert!(
result.code.contains("\"container\""),
"Should have container class mapping. Generated:\n{}",
result.code
);
assert!(
result.code.contains("._container_"),
"CSS should have hashed class name. Generated:\n{}",
result.code
);
}
#[test]
fn test_vite_returns_timing() {
let source = r#"<template><div>Hello</div></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
assert!(
result.duration_ms > 0.0,
"Should have positive timing. Got: {}",
result.duration_ms
);
}
#[test]
fn test_vite_extracts_script_block() {
let source = r#"<script setup lang="ts">
const msg = 'hello'
</script>
<template><div>{{ msg }}</div></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
sourcemap: true,
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
assert!(result.script.is_some(), "Should extract script block");
let script = result.script.as_ref().unwrap();
assert!(!script.code.is_empty(), "Script code should not be empty");
}
#[test]
fn test_vite_extracts_style_blocks() {
let source = r#"<template><div>test</div></template>
<style scoped lang="scss">.foo { color: red; }</style>
<style module>.bar { color: blue; }</style>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions::default();
let result = generate_for_vite(source, &options, &allocator);
assert_eq!(result.styles.len(), 2, "Should extract 2 style blocks");
assert!(result.styles[0].scoped, "First style should be scoped");
assert_eq!(
result.styles[0].lang,
Some("scss".to_string()),
"First style should have scss lang"
);
assert!(result.styles[1].is_module, "Second style should be module");
}
#[test]
fn test_vite_style_has_code() {
let source = r#"<template><div>test</div></template>
<style>.test { color: red; }</style>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions::default();
let result = generate_for_vite(source, &options, &allocator);
assert_eq!(result.styles.len(), 1, "Should extract 1 style block");
let style = &result.styles[0];
assert!(!style.code.is_empty(), "Style code should not be empty");
assert!(
style.code.contains("color: red"),
"Style should contain the CSS content"
);
}
#[test]
fn test_vite_validates_output_js() {
let source = r#"<template><div>test</div></template>
<script setup>const x = 1</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions::default();
let result = generate_for_vite(source, &options, &allocator);
let script = result.script.as_ref().expect("Should have script block");
assert_valid_js(&script.code, "vite script output");
}
#[test]
fn test_vite_sourcemap_option() {
let source = r#"<template><div>Hello</div></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options_no_map = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
sourcemap: false,
..Default::default()
};
let result_no_map = generate_for_vite(source, &options_no_map, &allocator);
let template = result_no_map
.template
.as_ref()
.expect("Should have template");
assert!(
template.source_map.is_none(),
"Should not have source map when disabled"
);
let options_with_map = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
sourcemap: true,
..Default::default()
};
let result_with_map = generate_for_vite(source, &options_with_map, &allocator);
let template = result_with_map
.template
.as_ref()
.expect("Should have template");
assert!(
template.source_map.is_some(),
"Should have source map when enabled"
);
}
#[test]
fn test_vite_define_props_type_params() {
let source1 = r#"<script setup lang="ts">
defineProps<{
store: string
}>()
</script>
<template><div>test</div></template>"#;
let allocator1 = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result1 = generate_for_vite(source1, &options, &allocator1);
let script1 = result1.script.as_ref().unwrap();
eprintln!(
"=== TEST 1: Resolvable type ===\n{}\n=== END ===",
script1.code
);
assert!(script1.code.contains("props:"), "Should have props");
assert_valid_js(&script1.code, "resolvable type props");
let source2 = r#"<script setup lang="ts">
defineProps<{
store: Store
}>()
</script>
<template><div>test</div></template>"#;
let allocator2 = oxc_allocator::Allocator::new();
let result2 = generate_for_vite(source2, &options, &allocator2);
let script2 = result2.script.as_ref().unwrap();
eprintln!(
"=== TEST 2: Unresolvable type ===\n{}\n=== END ===",
script2.code
);
assert!(script2.code.contains("props:"), "Should have props");
assert_valid_js(&script2.code, "unresolvable type props");
}
#[test]
fn test_vite_define_props_with_assignment() {
let source = r#"<script setup lang="ts">
const props = defineProps<{
direction?: 'horizontal' | 'vertical'
initialSplit?: number
}>()
</script>
<template><div>{{ props.direction }}</div></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
let script_code = &result.script.as_ref().unwrap().code;
assert!(
script_code.contains("props:"),
"Should have props section. Generated:\n{}",
script_code
);
assert!(
!script_code.contains("},)"),
"Should not have dangling paren after props. Generated:\n{}",
script_code
);
assert_valid_js(script_code, "defineProps with assignment");
}
#[test]
fn test_vite_define_props_with_imported_type() {
let source = r#"<script setup lang="ts">
import type { Store } from '../core/store'
defineProps<{
store: Store
}>()
</script>
<template><div>test</div></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("Header.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
let script_code = &result.script.as_ref().unwrap().code;
eprintln!(
"=== defineProps with imported type ===\n{}\n=== END ===",
script_code
);
assert!(
script_code.contains("props:"),
"Should have props section. Generated:\n{}",
script_code
);
assert!(
!script_code.contains("import type"),
"import type should be stripped from JS output. Generated:\n{}",
script_code
);
assert_valid_js(script_code, "defineProps with imported type");
}
#[test]
fn test_vite_split_blocks() {
let source = r#"<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div class="container">
<span>{{ count }}</span>
</div>
</template>
<style scoped>.container { color: red; }</style>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("SplitPane.vue".to_string()),
sourcemap: true,
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
let script = result.script.as_ref().expect("Should have script block");
eprintln!("=== SCRIPT BLOCK ===\n{}\n=== END ===", script.code);
assert!(
script.code.contains("_defineComponent") || script.code.contains("export default"),
"Script should contain component definition"
);
assert!(script.source_map.is_some(), "Script should have source map");
assert_valid_js(&script.code, "split script block");
let template = result
.template
.as_ref()
.expect("Should have template block");
eprintln!("=== TEMPLATE BLOCK ===\n{}\n=== END ===", template.code);
assert!(
template.code.contains("function render"),
"Template should contain render function. Got:\n{}",
template.code
);
assert!(
template.source_map.is_some(),
"Template should have source map"
);
assert_eq!(result.styles.len(), 1, "Should have 1 style block");
assert!(result.styles[0].scoped, "Style should be scoped");
assert!(
!result.styles[0].code.is_empty(),
"Style code should not be empty"
);
assert!(
result.styles[0].source_map.is_some(),
"Style should have source map"
);
}
#[test]
fn test_dev_setup_binding_uses_setup_prefix() {
let source = r#"<template><div>{{ count }}</div></template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>"#;
let code = gen_and_validate(source);
assert!(
code.contains("$setup.count"),
"Setup binding should use $setup. prefix. Generated:\n{}",
code
);
assert!(
!code.contains("_ctx.count"),
"Setup binding should NOT use _ctx. prefix. Generated:\n{}",
code
);
}
#[test]
fn test_dev_props_binding_uses_props_prefix() {
let source = r#"<template><div>{{ title }}</div></template>
<script setup>
const props = defineProps<{ title: string }>()
</script>"#;
let code = gen_and_validate(source);
assert!(
code.contains("$props.title"),
"Props should use $props. prefix in dev mode. Generated:\n{}",
code
);
assert!(
!code.contains("_ctx.title"),
"Props should NOT use _ctx. prefix in dev mode. Generated:\n{}",
code
);
}
#[test]
fn test_dev_mixed_setup_and_props() {
let source = r#"<template><div>{{ count }}: {{ title }}</div></template>
<script setup>
import { ref } from 'vue'
const props = defineProps<{ title: string }>()
const count = ref(0)
</script>"#;
let code = gen_and_validate(source);
assert!(
code.contains("$setup.count"),
"Setup binding should use $setup. Generated:\n{}",
code
);
assert!(
code.contains("$props.title"),
"Props should use $props. in dev mode. Generated:\n{}",
code
);
}
#[test]
fn test_render_signature_with_script_setup() {
let source = r#"<template><div>{{ msg }}</div></template>
<script setup>
const msg = 'hello'
</script>"#;
let code = gen_and_validate(source);
assert!(
code.contains("render(_ctx, _cache, $props, $setup, $data, $options)"),
"Dev render should have full signature. Generated:\n{}",
code
);
}
#[test]
fn test_template_before_script_ordering() {
let source = r#"<template><div>{{ count }}</div></template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>"#;
let code = gen_and_validate(source);
assert!(
code.contains("$setup.count"),
"Template-before-script should still resolve bindings. Generated:\n{}",
code
);
}
#[test]
fn test_ctx_slots_unchanged() {
let source = r#"<template><slot name="header" /></template>
<script setup></script>"#;
let code = gen_and_validate(source);
assert!(
code.contains("_ctx.$slots"),
"$slots should always use _ctx. prefix. Generated:\n{}",
code
);
}
#[test]
fn test_setup_component_direct_reference() {
let source = r#"<template><MyComponent /></template>
<script setup>
import MyComponent from './MyComponent.vue'
</script>"#;
let code = gen_and_validate(source);
assert!(
code.contains("$setup.MyComponent"),
"Setup component should use $setup. prefix in standalone mode. Generated:\n{}",
code
);
assert!(
!code.contains("_resolveComponent"),
"Should NOT use resolveComponent for setup components. Generated:\n{}",
code
);
}
#[test]
fn test_vite_path_binding_metadata() {
let source = r#"<template><div>{{ count }}</div></template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
let template_code = result.template.unwrap().code;
assert!(
template_code.contains("$setup.count"),
"Vite template should use $setup. prefix. Generated:\n{}",
template_code
);
}
#[test]
fn test_vite_path_component_resolution() {
let source = r#"<template><Header /></template>
<script setup>
import Header from './Header.vue'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
let template_code = result.template.unwrap().code;
assert!(
template_code.contains("$setup.Header"),
"Vite template should use $setup. prefix for setup components. Generated:\n{}",
template_code
);
assert!(
!template_code.contains("_resolveComponent"),
"Should NOT use resolveComponent for setup components. Generated:\n{}",
template_code
);
}
#[test]
fn test_vite_vif_without_velse_imports_comment_vnode() {
let source = r#"<script setup lang="ts">
defineProps<{
errors: string[]
}>()
</script>
<template>
<div v-if="errors.length > 0" class="message-container">
<div v-for="(error, i) in errors" :key="i" class="message error">
{{ error }}
</div>
</div>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("Message.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
let template_code = result.template.unwrap().code;
eprintln!(
"=== MESSAGE.VUE TEMPLATE ===\n{}\n=== END ===",
template_code
);
assert!(
template_code.contains("createCommentVNode"),
"v-if without v-else should import createCommentVNode. Generated:\n{}",
template_code
);
}
#[test]
fn test_vite_interpolation_in_vfor_with_function_call() {
let source = r#"<script setup>
const tabs = [{ mode: 'a', label: 'A' }]
function getTabTiming(mode) { return null }
</script>
<template>
<div>
<button v-for="tab in tabs" :key="tab.mode">
{{ tab.label }}
<span v-if="getTabTiming(tab.mode)" class="timing-pill">
{{ getTabTiming(tab.mode) }}
</span>
</button>
</div>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("Output.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
let template_code = result.template.unwrap().code;
eprintln!(
"=== OUTPUT.VUE TEMPLATE ===\n{}\n=== END ===",
template_code
);
let occurrences: Vec<_> = template_code.match_indices("getTabTiming").collect();
for (pos, _) in &occurrences {
let before = &template_code[pos.saturating_sub(10)..*pos];
eprintln!("getTabTiming at {}: ...{}getTabTiming...", pos, before);
}
assert!(
!template_code.contains("_toDisplayString(getTabTiming("),
"Interpolation should prefix getTabTiming with $setup. Generated:\n{}",
template_code
);
}
#[test]
fn test_vite_scope_id_uses_component_id_option() {
let source = r#"<template><div class="box">hi</div></template>
<style scoped>.box { color: red; }</style>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
component_id: Some("abcd1234".to_string()),
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
let template_code = result.template.unwrap().code;
let style_code = &result.styles[0].code;
assert!(
!template_code.contains("data-v-"),
"Template should NOT contain data-v- scope attributes. Generated:\n{}",
template_code
);
assert!(
style_code.contains("[data-v-abcd1234]"),
"CSS should use component_id from options. Generated:\n{}",
style_code
);
}
#[test]
fn test_implicit_void_tags_generate_valid_js() {
let source = r#"<template>
<div>
<br>
<input type="text">
<hr>
<img src="test.png">
</div>
</template>
<script setup>
</script>"#;
let code = gen_and_validate(source);
assert!(
code.contains("_createElementVNode(\"br\""),
"Should generate br element. Generated:\n{}",
code
);
assert!(
code.contains("_createElementVNode(\"input\""),
"Should generate input element. Generated:\n{}",
code
);
assert!(
code.contains("_createElementVNode(\"hr\""),
"Should generate hr element. Generated:\n{}",
code
);
assert!(
code.contains("_createElementVNode(\"img\""),
"Should generate img element. Generated:\n{}",
code
);
}
#[test]
fn e2e_root_component_uses_create_block_render_matches_vue() {
let vue_source = r#"<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>"#;
let vue_render = r#"import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
const _component_RouterView = _resolveComponent("RouterView")
return (_openBlock(), _createBlock(_component_RouterView))
}"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "root component createBlock render output");
let diffs = compare_ast_structure(&our_render, vue_render, "render_block");
assert!(
diffs.is_empty(),
"Render output differs from Vue:\n{}\n\nVerter:\n{}\n\nVue:\n{}",
diffs.join("\n"),
our_render,
vue_render
);
}
#[test]
fn e2e_prod_mode_standalone_template_is_correct() {
let vue_source = r#"<script setup lang="ts">
import { ref } from "vue";
const split = ref(50);
</script>
<template>
<div v-if="true"
:style="{ flexBasis: split + '%' }" />
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("ProdTest.vue".to_string()),
is_production: true,
ssr: false,
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
assert!(
result.template.is_none(),
"Production <script setup> should inline template (no separate template block)"
);
let script = result
.script
.expect("should have script block in prod mode");
let our_code = &script.code;
assert!(
!our_code.contains(r#"_createCommentVNode("v-if""#),
"Prod mode must NOT contain createCommentVNode(\"v-if\").\n\nGenerated:\n{}",
our_code
);
assert!(
our_code.contains(r#"_createCommentVNode("", true)"#),
"Prod mode must contain createCommentVNode(\"\", true).\n\nGenerated:\n{}",
our_code
);
assert!(
our_code.contains("return (_ctx, _cache) =>"),
"Inline mode must return render arrow function from setup.\n\nGenerated:\n{}",
our_code
);
assert!(
!our_code.contains("function render("),
"Inline mode must NOT have separate render function.\n\nGenerated:\n{}",
our_code
);
assert!(
!our_code.contains("$setup.split"),
"Inline mode must NOT use $setup. prefix.\n\nGenerated:\n{}",
our_code
);
}
#[test]
fn e2e_prod_mode_standalone_props_alias() {
let vue_source = r#"<script setup lang="ts">
const props = defineProps<{ direction?: string }>();
</script>
<template>
<div :class="props.direction" />
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("PropsAlias.vue".to_string()),
is_production: true,
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
assert!(
result.template.is_none(),
"Production <script setup> should inline template"
);
let script = result.script.expect("should have script block");
let our_code = &script.code;
assert!(
our_code.contains("__props.direction")
|| our_code.contains("$setup.props.direction")
|| our_code.contains("props.direction"),
"Props alias must resolve correctly in inline mode.\n\nGenerated:\n{}",
our_code
);
}
#[test]
fn e2e_vfor_keyed_uses_keyed_fragment() {
let vue_source = r#"<script setup>
import { ref } from "vue";
const versions = ref([{ id: 1, label: "v1" }, { id: 2, label: "v2" }]);
</script>
<template>
<div v-for="entry in versions" :key="entry.id">
{{ entry.label }}
</div>
</template>"#;
let vue_render = r#"import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList($setup.versions, (entry) => {
return (_openBlock(), _createElementBlock("div", { key: entry.id }, _toDisplayString(entry.label), 1))
}), 128))
}"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "keyed v-for render output");
assert!(
our_render.contains("128") || our_render.contains("KEYED_FRAGMENT"),
"Keyed v-for with reactive ref source must use KEYED_FRAGMENT (128).\n\nGenerated:\n{}",
our_render
);
assert!(
!our_render.contains("64 /* STABLE_FRAGMENT */") && !our_render.contains(", 64)"),
"Keyed v-for with reactive ref source must NOT use STABLE_FRAGMENT (64).\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_template_ref_uses_ref_key_pattern() {
let vue_source = r#"<script setup>
import { ref } from "vue";
const container = ref(null);
</script>
<template>
<div ref="container">content</div>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "template ref render output");
assert!(
our_render.contains("ref_key: \"container\""),
"Template ref matching setup binding must use ref_key pattern.\n\nGenerated:\n{}",
our_render
);
assert!(
our_render.contains("ref: $setup.container") || our_render.contains("ref: container"),
"Template ref must reference the actual variable, not a string.\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_setup_component_uses_direct_reference() {
let vue_source = r#"<script setup>
import MyChild from "./MyChild.vue";
</script>
<template>
<MyChild msg="hello" />
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "setup component render output");
assert!(
!our_render.contains("resolveComponent"),
"Script setup components should not use resolveComponent.\n\nGenerated:\n{}",
our_render
);
assert!(
our_render.contains("$setup.MyChild") || our_render.contains("$setup[\"MyChild\"]"),
"Script setup components should be referenced via $setup.\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_prod_mode_hoisted_at_module_level() {
let vue_source = r#"<script setup lang="ts">
import { ref } from "vue";
const msg = ref("hello");
</script>
<template>
<div class="container">{{ msg }}</div>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
is_production: true,
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
assert!(
result.template.is_none(),
"Production <script setup> should inline template"
);
let script = result.script.expect("should have script block");
let our_code = &script.code;
if our_code.contains("_hoisted_") {
let hoisted_pos = our_code.find("_hoisted_").unwrap();
let setup_pos = our_code.find("setup(").expect("should contain setup(");
assert!(
hoisted_pos < setup_pos,
"Hoisted variables must appear before setup() (at module level), not inside setup().\n\nGenerated:\n{}",
our_code
);
}
}
#[test]
fn e2e_prod_mode_strips_patch_flag_comments() {
let vue_source = r#"<script setup>
import { ref } from "vue";
const items = ref([]);
const msg = ref("hello");
</script>
<template>
<div>
<span>{{ msg }}</span>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let mut options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
options.is_production = true;
let result = generate_for_vite(vue_source, &options, &allocator);
assert!(
result.template.is_none(),
"Production <script setup> should inline template"
);
let script = result.script.expect("should have script block");
let our_code = &script.code;
let code_without_pure = our_code.replace("/*@__PURE__*/", "");
assert!(
!code_without_pure.contains("/*"),
"Production output should not contain patch flag comments.\n\nGenerated:\n{}",
our_code
);
}
#[test]
fn e2e_component_slot_text_after_self_closing_child_has_comma() {
let vue_source = r#"<template><CHeader> <CIcon /> Theme colors </CHeader></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "component slot with mixed children");
}
#[test]
fn e2e_hyphenated_directive_name_camelized_in_variable() {
let vue_source = r#"<template><div v-my-custom="val">text</div></template>
<script setup>const val = 'x'</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "hyphenated directive render output");
assert!(
our_render.contains("_directive_myCustom"),
"Directive variable should be camelized to _directive_myCustom.\n\nGenerated:\n{}",
our_render
);
assert!(
!our_render.contains("_directive_my-custom"),
"Directive variable should NOT contain hyphens.\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_custom_directive_on_component_with_children() {
let vue_source =
r#"<template><CLink v-c-tooltip="'Tooltip text'">link text</CLink></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "directive on component with children");
assert!(
our_render.contains("_withDirectives("),
"Should wrap component in _withDirectives.\n\nGenerated:\n{}",
our_render
);
assert!(
our_render.contains("_directive_cTooltip"),
"Should have camelized directive variable.\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_custom_directive_on_self_closing_component() {
let vue_source = r#"<template><CIcon v-c-tooltip="'Icon tooltip'" /></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "directive on self-closing component");
assert!(
our_render.contains("_withDirectives("),
"Should wrap component in _withDirectives.\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_component_slot_interpolation_only() {
let vue_source = r#"<template><CToastBody>{{ toast.content }}</CToastBody></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "component with interpolation-only child");
assert!(
our_render.contains("_withCtx("),
"Should have slot wrapper with _withCtx.\n\nGenerated:\n{}",
our_render
);
assert!(
our_render.contains("_createTextVNode("),
"Should wrap interpolation in _createTextVNode.\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_multiline_attribute_value_escaped() {
let vue_source = "<template><CFormCheck label=\"Radio\n 1\" /></template>";
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "multiline attribute value");
}
#[test]
fn e2e_cached_self_closing_element_in_vfor() {
let vue_source = r#"<template><div v-for="item in items"><hr class="mt-0" /></div></template>
<script setup>const items = [1,2]</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "cached element in v-for");
assert!(
our_render.contains("_createElementVNode(\"hr\""),
"Should have _createElementVNode for hr.\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_component_slot_text_with_sibling_component() {
let vue_source = r##"<template><CAlert color="primary">A simple alert with <CAlertLink href="#">a link</CAlertLink>. Give it a click.</CAlert></template>"##;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(
&our_render,
"component with mixed text and component children",
);
}
#[test]
fn e2e_directive_with_object_literal_value() {
let vue_source = r#"<template><button v-c-popover="{header: 'Popover', content: 'And here is some content'}">Click</button></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "directive with object literal value");
assert!(
!our_render.contains("_ctx.{"),
"Object literal should not get _ctx. prefix.\n\nGenerated:\n{}",
our_render
);
assert!(
our_render.contains("{header: 'Popover', content: 'And here is some content'}"),
"Should contain the object literal value.\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_directive_with_object_literal_on_component() {
let vue_source = r#"<template><CButton v-c-tooltip="{content: 'tooltip text', placement: 'top'}">Hover</CButton></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "directive with object literal on component");
assert!(
!our_render.contains("_ctx.{"),
"Object literal should not get _ctx. prefix on component.\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_vfor_on_component_with_text_child() {
let vue_source = r#"<template>
<CBreadcrumb class="my-0">
<CBreadcrumbItem
v-for="item in breadcrumbs"
:key="item"
:href="item.active ? '' : item.path"
:active="item.active"
>
{{ item.name }}
</CBreadcrumbItem>
</CBreadcrumb>
</template>
<script setup>
import { ref } from 'vue'
const breadcrumbs = ref([])
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "vfor_on_component_with_text_child");
}
#[test]
fn e2e_vif_velse_chain_in_component_slot() {
let vue_source = r#"<template>
<CDropdown>
<CDropdownToggle :caret="false">
<CIcon v-if="colorMode === 'dark'" icon="cil-moon" size="lg" />
<CIcon v-else-if="colorMode === 'light'" icon="cil-sun" size="lg" />
<CIcon v-else icon="cil-contrast" size="lg" />
</CDropdownToggle>
</CDropdown>
</template>
<script setup>
import { ref } from 'vue'
const colorMode = ref('auto')
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "vif_velse_chain_in_component_slot");
}
#[test]
fn e2e_dynamic_class_array_binding_on_component() {
let vue_source = r#"<template>
<CTabContent :class="['rounded-bottom', addClass]">
<slot></slot>
</CTabContent>
</template>
<script setup>
const addClass = 'extra'
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "dynamic_class_array_binding_on_component");
}
#[test]
fn e2e_mixed_text_and_component_children() {
let vue_source = r#"<template>
<CLink href="https://example.com">
View more
<CIcon icon="cil-arrow-right" width="16" />
</CLink>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "mixed_text_and_component_children");
}
#[test]
fn e2e_named_slot_with_text_and_inline_component() {
let vue_source = r#"<template>
<CWidgetStatsA>
<template #value>
26K
<span class="fs-6 fw-normal"> (-12.4% <CIcon icon="cil-arrow-bottom" />) </span>
</template>
</CWidgetStatsA>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "named_slot_with_text_and_inline_component");
}
#[test]
fn e2e_custom_directive_multiline_object_literal() {
let vue_source = r#"<template>
<CButton
v-c-popover="{
header: 'Popover title',
content: 'Some amazing content',
placement: 'right',
}"
color="danger"
size="lg"
>
Click to toggle popover
</CButton>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "custom_directive_multiline_object_literal");
assert!(
!our_render.contains("_ctx.{"),
"Object literal should not get _ctx. prefix.\n\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_multiple_named_slots_with_text_and_components() {
let vue_source = r#"<template>
<CWidgetStatsA color="primary">
<template #value>26K
<span class="fs-6 fw-normal"> (-12.4% <CIcon icon="cil-arrow-bottom" />) </span>
</template>
<template #title>Users</template>
<template #action>
<CDropdown placement="bottom-end">
<CDropdownToggle color="transparent" :caret="false">
<CIcon icon="cil-options" />
</CDropdownToggle>
</CDropdown>
</template>
</CWidgetStatsA>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "multiple_named_slots_with_text_and_components");
}
#[test]
fn e2e_component_with_text_and_element_children_mixed() {
let vue_source = r#"<template>
<CLink
class="fw-semibold font-xs text-body-secondary"
href="https://coreui.io/"
rel="noopener norefferer"
target="_blank"
>
View more
<CIcon icon="cil-arrow-right" class="ms-auto" width="16" />
</CLink>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(
&our_render,
"component_with_text_and_element_children_mixed",
);
}
#[test]
fn e2e_component_with_multiple_element_children() {
let vue_source = r#"<template><CFooter><div>A</div><div>B</div></CFooter></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "component_with_multiple_element_children");
assert!(
!our_render.contains(", ,"),
"Must not have double commas between slot children. Output:\n{}",
our_render
);
}
#[test]
fn e2e_component_with_text_child_inside_div() {
let vue_source =
r#"<template><div><CHeader><CIcon /> Theme colors</CHeader></div></template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "component_with_text_child_inside_div");
}
#[test]
fn e2e_docs_example_nested_components_with_text() {
let vue_source = r##"<script setup>
const props = defineProps({
href: String,
tabContentClass: String,
})
const url = `https://coreui.io/vue/docs/${props.href}`
const addClass = props.tabContentClass
</script>
<template>
<div class="example">
<CNav variant="underline-border">
<CNavItem>
<CNavLink href="#" active>
<CIcon icon="cil-media-play" class="me-2" />
Preview
</CNavLink>
</CNavItem>
<CNavItem>
<CNavLink :href="url" target="_blank">
<CIcon icon="cil-code" class="me-2" />
Code
</CNavLink>
</CNavItem>
</CNav>
<CTabContent :class="['rounded-bottom', addClass]">
<CTabPane class="p-3 preview" visible>
<slot></slot>
</CTabPane>
</CTabContent>
</div>
</template>"##;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "docs_example_nested_components_with_text");
}
#[test]
fn e2e_kebab_case_tag_treated_as_component() {
let vue_source = r#"<template>
<v-alert type="success">Hello</v-alert>
</template>
<script setup>
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "kebab_case_component");
assert!(
our_render.contains("_resolveComponent(\"v-alert\")"),
"Should resolve v-alert as a component.\nGenerated:\n{}",
our_render
);
assert!(
our_render.contains("_component_v_alert"),
"Component variable should replace hyphens with underscores.\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_kebab_case_template_only_component() {
let vue_source = r#"<template>
<v-layout class="rounded">
<v-app-bar title="Test"></v-app-bar>
<v-main>Content</v-main>
</v-layout>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "kebab_case_template_only");
assert!(
our_render.contains("_resolveComponent(\"v-layout\")"),
"Should resolve v-layout.\nGenerated:\n{}",
our_render
);
assert!(
our_render.contains("_resolveComponent(\"v-app-bar\")"),
"Should resolve v-app-bar.\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_kebab_case_with_named_slots() {
let vue_source = r#"<template>
<v-alert type="info" border="start">
<template #prepend>
<v-icon color="blue" icon="mdi-check" />
</template>
<slot />
</v-alert>
</template>
<script setup>
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "kebab_case_named_slots");
assert!(
our_render.contains("_resolveComponent(\"v-alert\")"),
"Should resolve v-alert.\nGenerated:\n{}",
our_render
);
assert!(
our_render.contains("_resolveComponent(\"v-icon\")"),
"Should resolve v-icon.\nGenerated:\n{}",
our_render
);
}
#[test]
fn e2e_vite_regular_script_no_raw_tags() {
let vue_source = r#"<template>
<div class="wrapper">
<slot />
</div>
</template>
<script>
export default {
inheritAttrs: false,
}
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let script = result.script.expect("should have script block").code;
assert!(
!script.contains("<script>"),
"Script output should not contain <script> tag.\nGenerated:\n{}",
script
);
assert!(
!script.contains("</script>"),
"Script output should not contain </script> tag.\nGenerated:\n{}",
script
);
assert!(
script.contains("export default"),
"Should preserve export default.\nGenerated:\n{}",
script
);
}
#[test]
fn e2e_vite_dual_script_no_raw_tags() {
let vue_source = r#"<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
inheritAttrs: false,
}
</script>
<script setup>
import { ref } from 'vue'
const msg = ref('hello')
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let script = result.script.expect("should have script block").code;
assert!(
!script.contains("<script>"),
"Script output should not contain raw <script> tag.\nGenerated:\n{}",
script
);
assert!(
!script.contains("</script>"),
"Script output should not contain raw </script> tag.\nGenerated:\n{}",
script
);
assert!(
script.contains("setup("),
"Should have setup function.\nGenerated:\n{}",
script
);
assert!(
script.contains("const __default__"),
"Should have __default__ from regular script.\nGenerated:\n{}",
script
);
assert!(
script.contains("Object.assign(__default__"),
"Should merge via Object.assign.\nGenerated:\n{}",
script
);
}
#[test]
fn e2e_vite_dual_script_setup_first() {
let vue_source = r#"<template>
<div>{{ msg }}</div>
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('hello')
</script>
<script>
export default {
inheritAttrs: false,
}
</script>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let script = result.script.expect("should have script block").code;
assert!(
!script.contains("<script>"),
"Should not contain <script>.\nGenerated:\n{}",
script
);
assert!(
script.contains("const __default__"),
"Should have __default__ from regular script.\nGenerated:\n{}",
script
);
assert!(
script.contains("Object.assign(__default__"),
"Should merge via Object.assign even when setup comes first.\nGenerated:\n{}",
script
);
}
#[test]
fn e2e_conditional_named_slot_with_default_children() {
let vue_source = r#"<template>
<v-app-bar v-bind="props">
<template v-slot:prepend>
<v-app-bar-nav-icon></v-app-bar-nav-icon>
</template>
<v-app-bar-title>Application Bar</v-app-bar-title>
<template v-if="actions" v-slot:append>
<v-btn icon="mdi-heart"></v-btn>
<v-btn icon="mdi-magnify"></v-btn>
</template>
</v-app-bar>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "conditional named slot with default children");
}
#[test]
fn e2e_vshow_negation_expression() {
let vue_source = r#"<template>
<div v-show="!hidden">content</div>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "v-show with negation");
}
#[test]
fn e2e_mixed_text_element_children() {
let vue_source = r#"<template>
<div class="px-4 py-2">
{{ year }} — <strong>Vuetify</strong>
</div>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "mixed text+element children");
}
#[test]
fn e2e_conditional_named_slot() {
let vue_source = r#"<template>
<v-list-item>
<template v-if="showPrepend" #prepend>
<v-avatar image="logo.png" />
</template>
<v-list-item-title>Title</v-list-item-title>
</v-list-item>
</template>"#;
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(vue_source, &options, &allocator);
let our_render = result.template.expect("should have template block").code;
assert_valid_js(&our_render, "conditional named slot");
}
#[test]
fn e2e_vuetify_matrix_failing_patterns() {
let cases: Vec<(&str, &str)> = vec![
(
"script_vue",
r#"<template>
<div :id="id" ref="rootEl" />
</template>
<script setup lang="ts">
import { onBeforeMount, onBeforeUnmount, onMounted, ref } from 'vue'
const props = defineProps({
id: { type: String, required: true },
scriptId: { type: String, required: true },
src: { type: String, required: true },
})
const emit = defineEmits(['script:error', 'script:load'])
const rootEl = ref<HTMLElement>()
</script>"#,
),
(
"inline_vue",
r#"<template>
<app-markdown
v-if="ad"
:content="description"
class="v-markdown--inline d-inline"
tag="span"
/>
</template>
<script setup>
import { createAdProps, useAd } from '@/composables/ad'
const props = defineProps(createAdProps())
const { ad, description } = useAd(props)
</script>"#,
),
(
"entry_vue",
r#"<template>
<carbon v-if="!user.disableAds" />
<br>
<app-btn
v-if="false"
:text="user.disableAds ? 'enable' : 'disable'"
class="text-caption"
color="surface-variant"
prepend-icon="$vuetify"
variant="flat"
@click="onClickDisableAds"
/>
</template>
<script setup>
import { useUserStore } from '@/store/user'
const user = useUserStore()
function onClickDisableAds () { user.disableAds = !user.disableAds }
</script>"#,
),
(
"default_vue",
r#"<template>
<v-app>
<v-main>
<v-container fluid tag="section">
<router-view v-slot="{ Component }">
<v-fade-transition hide-on-leave>
<div :key="route.name">
<component :is="Component" />
</div>
</v-fade-transition>
</router-view>
</v-container>
</v-main>
</v-app>
</template>
<script setup>
import { useRoute } from 'vue-router'
import { computed } from 'vue'
const route = useRoute()
</script>"#,
),
(
"layout_composable",
r#"<template>
<v-layout ref="app" class="rounded rounded-md">
<v-app-bar color="grey-lighten-2" name="app-bar">
<child v-slot="{ print }">
<v-btn class="mx-auto" @click="print('app-bar')">Get data</v-btn>
</child>
</v-app-bar>
<v-main class="d-flex" style="min-height: 300px;">
Main Content
</v-main>
</v-layout>
</template>
<script setup>
import { useLayout } from 'vuetify'
</script>"#,
),
(
"search_vue",
r#"<!-- eslint-disable -->
<template>
<v-dialog v-model="model" scrollable width="600">
<template #activator="{ props: activatorProps }">
<app-btn :active="model" v-bind="activatorProps">
Search
</app-btn>
</template>
</v-dialog>
</template>
<script setup>
import { ref } from 'vue'
const model = ref(false)
</script>"#,
),
(
"list_vue",
r#"<template>
<v-list
v-model:opened="opened"
:nav="nav"
:items="computedItems"
color="primary"
density="compact"
>
<template #divider>
<v-divider class="my-3 mb-4 ms-2 me-n2" />
</template>
<template #title="{ item }">
{{ item.title }}
<v-badge v-if="item.emphasized" class="ms-n1" color="success" dot inline />
</template>
<template #subtitle="{ item }">
<span v-if="item.subtitle" class="text-high-emphasis">
{{ item.subtitle }}
</span>
</template>
</v-list>
</template>
<script setup>
import { ref, computed } from 'vue'
const opened = ref([])
const props = defineProps({ items: Array, nav: Boolean })
const computedItems = computed(() => props.items)
</script>"#,
),
(
"vee_validate",
r#"<template>
<form @submit.prevent="submit">
<v-text-field
v-model="name.value.value"
:counter="10"
:error-messages="name.errorMessage.value"
label="Name"
></v-text-field>
<v-btn class="me-4" type="submit">submit</v-btn>
<v-btn @click="handleReset">clear</v-btn>
</form>
</template>
<script setup>
import { ref } from 'vue'
const name = ref({ value: { value: '' }, errorMessage: { value: '' } })
const handleReset = () => {}
const submit = () => {}
</script>"#,
),
];
let mut failures: Vec<String> = Vec::new();
for (name, source) in &cases {
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
if let Some(ref template) = result.template {
let alloc2 = oxc_allocator::Allocator::default();
let source_type = SourceType::mjs();
let p = Parser::new(&alloc2, &template.code, source_type).parse();
if !p.errors.is_empty() {
failures.push(format!(
"{} (TEMPLATE): {:?}\n---\n{}\n---",
name, p.errors, template.code
));
}
} else {
failures.push(format!("{}: NO TEMPLATE OUTPUT", name));
}
if let Some(ref script) = result.script {
let alloc2 = oxc_allocator::Allocator::default();
let source_type = if source.contains("lang=\"ts\"") || source.contains("lang='ts'")
{
SourceType::ts()
} else {
SourceType::mjs()
};
let p = Parser::new(&alloc2, &script.code, source_type).parse();
if !p.errors.is_empty() {
failures.push(format!(
"{} (SCRIPT): {:?}\n---\n{}\n---",
name, p.errors, script.code
));
}
}
}
if !failures.is_empty() {
panic!(
"\n{} failures:\n\n{}",
failures.len(),
failures.join("\n\n")
);
}
}
#[test]
fn e2e_debug_sibling_comma() {
let source_simple = r#"<template>
<v-layout>
<v-app-bar></v-app-bar>
<v-main>Main</v-main>
</v-layout>
</template>"#;
let source_child = r#"<template>
<v-layout>
<v-app-bar>
<child>
<v-btn>Get data</v-btn>
</child>
</v-app-bar>
<v-main>Main</v-main>
</v-layout>
</template>"#;
let source_vslot = r#"<template>
<v-layout>
<v-app-bar>
<child v-slot="{ print }">
<v-btn>Get data</v-btn>
</child>
</v-app-bar>
<v-main>Main</v-main>
</v-layout>
</template>"#;
for (label, source) in [
("simple", source_simple),
("child", source_child),
("vslot", source_vslot),
] {
let allocator = oxc_allocator::Allocator::new();
let options = ViteCodegenOptions {
filename: Some("test.vue".to_string()),
..Default::default()
};
let result = generate_for_vite(source, &options, &allocator);
let template = result.template.as_ref().unwrap();
eprintln!("=== {} ===\n{}\n", label, template.code);
let alloc2 = oxc_allocator::Allocator::default();
let source_type = SourceType::mjs();
let p = Parser::new(&alloc2, &template.code, source_type).parse();
assert!(
p.errors.is_empty(),
"{} has parse errors: {:?}\n---\n{}",
label,
p.errors,
template.code
);
}
}
}