mod children;
mod context;
mod element;
mod expression;
mod helpers;
mod node;
mod patch_flag;
mod props;
mod slots;
mod v_for;
mod v_if;
use crate::ast::*;
use crate::options::CodegenOptions;
pub use context::{CodegenContext, CodegenResult};
use element::generate_root_node;
use helpers::escape_js_string;
use node::generate_node;
fn is_ignorable_root_text(child: &TemplateChildNode<'_>) -> bool {
matches!(child, TemplateChildNode::Text(text) if text.content.chars().all(|c| c.is_whitespace()))
}
pub fn generate(root: &RootNode<'_>, options: CodegenOptions) -> CodegenResult {
let mut ctx = CodegenContext::new(options);
let root_children: std::vec::Vec<&TemplateChildNode<'_>> = root
.children
.iter()
.filter(|child| !is_ignorable_root_text(child))
.collect();
generate_function_signature(&mut ctx);
ctx.indent();
ctx.newline();
generate_assets(&mut ctx, root);
ctx.push("return ");
if root_children.is_empty() {
ctx.push("null");
} else if root_children.len() == 1 {
generate_root_node(&mut ctx, root_children[0]);
} else {
ctx.use_helper(RuntimeHelper::OpenBlock);
ctx.use_helper(RuntimeHelper::CreateElementBlock);
ctx.use_helper(RuntimeHelper::Fragment);
ctx.push("(");
ctx.push(ctx.helper(RuntimeHelper::OpenBlock));
ctx.push("(), ");
ctx.push(ctx.helper(RuntimeHelper::CreateElementBlock));
ctx.push("(");
ctx.push(ctx.helper(RuntimeHelper::Fragment));
ctx.push(", null, [");
ctx.indent();
for (i, child) in root_children.iter().enumerate() {
if i > 0 {
ctx.push(",");
}
ctx.newline();
generate_node(&mut ctx, child);
}
ctx.deindent();
ctx.newline();
ctx.push("], 64 /* STABLE_FRAGMENT */))");
}
ctx.deindent();
ctx.newline();
ctx.push("}");
let mut all_helpers: Vec<RuntimeHelper> = ctx.used_helpers.iter().copied().collect();
if root.helpers.contains(&RuntimeHelper::Unref) && !all_helpers.contains(&RuntimeHelper::Unref)
{
all_helpers.push(RuntimeHelper::Unref);
}
collect_hoist_helpers(root, &mut all_helpers);
all_helpers.sort();
all_helpers.dedup();
let mut preamble = generate_preamble_from_helpers(&ctx, &all_helpers);
let hoists_code = generate_hoists(&ctx, root);
if !hoists_code.is_empty() {
preamble.push('\n');
preamble.push_str(&hoists_code);
}
CodegenResult {
code: ctx.into_code(),
preamble,
map: None,
}
}
fn generate_preamble_from_helpers(ctx: &CodegenContext, helpers: &[RuntimeHelper]) -> String {
if helpers.is_empty() {
return String::new();
}
let estimated_capacity = 32 + helpers.len() * 24;
let mut preamble = Vec::with_capacity(estimated_capacity);
match ctx.options.mode {
crate::options::CodegenMode::Module => {
preamble.extend_from_slice(b"import { ");
for (i, h) in helpers.iter().enumerate() {
if i > 0 {
preamble.extend_from_slice(b", ");
}
preamble.extend_from_slice(h.name().as_bytes());
preamble.extend_from_slice(b" as ");
preamble.extend_from_slice(ctx.helper(*h).as_bytes());
}
preamble.extend_from_slice(b" } from \"");
preamble.extend_from_slice(ctx.runtime_module_name.as_bytes());
preamble.extend_from_slice(b"\"\n");
}
crate::options::CodegenMode::Function => {
preamble.extend_from_slice(b"const { ");
for (i, h) in helpers.iter().enumerate() {
if i > 0 {
preamble.extend_from_slice(b", ");
}
preamble.extend_from_slice(h.name().as_bytes());
preamble.extend_from_slice(b": ");
preamble.extend_from_slice(ctx.helper(*h).as_bytes());
}
preamble.extend_from_slice(b" } = ");
preamble.extend_from_slice(ctx.runtime_global_name.as_bytes());
preamble.push(b'\n');
}
}
unsafe { String::from_utf8_unchecked(preamble) }
}
fn generate_function_signature(ctx: &mut CodegenContext) {
if ctx.options.ssr {
ctx.push("function ssrRender(_ctx, _push, _parent, _attrs) {");
} else {
match ctx.options.mode {
crate::options::CodegenMode::Module => {
if ctx.options.binding_metadata.is_some() {
ctx.push(
"export function render(_ctx, _cache, $props, $setup, $data, $options) {",
);
} else {
ctx.push("export function render(_ctx, _cache) {");
}
}
crate::options::CodegenMode::Function => {
ctx.push("function render(_ctx, _cache, $props, $setup, $data, $options) {");
}
}
}
}
fn generate_hoists(ctx: &CodegenContext, root: &RootNode<'_>) -> String {
let mut hoists_code: Vec<u8> = Vec::new();
for (i, hoist) in root.hoists.iter().enumerate() {
if let Some(node) = hoist {
hoists_code.extend_from_slice(b"const _hoisted_");
hoists_code.extend_from_slice((i + 1).to_string().as_bytes());
hoists_code.extend_from_slice(b" = ");
if matches!(node, JsChildNode::VNodeCall(_)) {
hoists_code.extend_from_slice(b"/*#__PURE__*/ ");
}
generate_js_child_node_to_bytes(ctx, node, &mut hoists_code);
hoists_code.push(b'\n');
}
}
unsafe { String::from_utf8_unchecked(hoists_code) }
}
fn collect_hoist_helpers(root: &RootNode<'_>, helpers: &mut Vec<RuntimeHelper>) {
for node in root.hoists.iter().flatten() {
collect_helpers_from_js_child_node(node, helpers);
}
}
fn collect_helpers_from_js_child_node(node: &JsChildNode<'_>, helpers: &mut Vec<RuntimeHelper>) {
match node {
JsChildNode::VNodeCall(vnode) => collect_helpers_from_vnode_call(vnode, helpers),
JsChildNode::Object(obj) => {
for prop in &obj.properties {
collect_helpers_from_js_child_node(&prop.value, helpers);
}
}
_ => {}
}
}
fn collect_helpers_from_vnode_call(vnode: &VNodeCall<'_>, helpers: &mut Vec<RuntimeHelper>) {
if vnode.is_block {
helpers.push(RuntimeHelper::OpenBlock);
if vnode.is_component {
helpers.push(RuntimeHelper::CreateBlock);
} else {
helpers.push(RuntimeHelper::CreateElementBlock);
}
} else if vnode.is_component {
helpers.push(RuntimeHelper::CreateVNode);
} else {
helpers.push(RuntimeHelper::CreateElementVNode);
}
if let VNodeTag::Symbol(helper) = &vnode.tag {
helpers.push(*helper);
}
if let Some(props) = &vnode.props {
collect_helpers_from_props(props, helpers);
}
}
fn collect_helpers_from_props(props: &PropsExpression<'_>, helpers: &mut Vec<RuntimeHelper>) {
if let PropsExpression::Object(obj) = props {
for prop in &obj.properties {
collect_helpers_from_js_child_node(&prop.value, helpers);
}
}
}
fn generate_js_child_node_to_bytes(
ctx: &CodegenContext,
node: &JsChildNode<'_>,
out: &mut Vec<u8>,
) {
match node {
JsChildNode::VNodeCall(vnode) => generate_vnode_call_to_bytes(ctx, vnode, out),
JsChildNode::SimpleExpression(exp) => {
if exp.is_static {
out.push(b'"');
let escaped = crate::codegen::helpers::escape_js_string(&exp.content);
out.extend_from_slice(escaped.as_bytes());
out.push(b'"');
} else {
out.extend_from_slice(exp.content.as_bytes());
}
}
JsChildNode::Object(obj) => {
out.extend_from_slice(b"{ ");
for (i, prop) in obj.properties.iter().enumerate() {
if i > 0 {
out.extend_from_slice(b", ");
}
match &prop.key {
ExpressionNode::Simple(exp) => {
let key = &exp.content;
let needs_quote = !crate::codegen::helpers::is_valid_js_identifier(key);
if needs_quote {
out.push(b'"');
out.extend_from_slice(key.as_bytes());
out.push(b'"');
} else {
out.extend_from_slice(key.as_bytes());
}
out.extend_from_slice(b": ");
}
ExpressionNode::Compound(_) => out.extend_from_slice(b"null: "),
}
generate_js_child_node_to_bytes(ctx, &prop.value, out);
}
out.extend_from_slice(b" }");
}
_ => out.extend_from_slice(b"null /* unsupported */"),
}
}
fn generate_vnode_call_to_bytes(ctx: &CodegenContext, vnode: &VNodeCall<'_>, out: &mut Vec<u8>) {
if vnode.is_block {
out.push(b'(');
out.extend_from_slice(ctx.helper(RuntimeHelper::OpenBlock).as_bytes());
out.extend_from_slice(b"(), ");
if vnode.is_component {
out.extend_from_slice(ctx.helper(RuntimeHelper::CreateBlock).as_bytes());
} else {
out.extend_from_slice(ctx.helper(RuntimeHelper::CreateElementBlock).as_bytes());
}
} else if vnode.is_component {
out.extend_from_slice(ctx.helper(RuntimeHelper::CreateVNode).as_bytes());
} else {
out.extend_from_slice(ctx.helper(RuntimeHelper::CreateElementVNode).as_bytes());
}
out.push(b'(');
match &vnode.tag {
VNodeTag::String(s) => {
out.push(b'"');
out.extend_from_slice(s.as_bytes());
out.push(b'"');
}
VNodeTag::Symbol(helper) => out.extend_from_slice(ctx.helper(*helper).as_bytes()),
VNodeTag::Call(_) => out.extend_from_slice(b"null"),
}
if let Some(props) = &vnode.props {
out.extend_from_slice(b", ");
generate_props_expression_to_bytes(ctx, props, out);
} else if vnode.children.is_some() || vnode.patch_flag.is_some() {
out.extend_from_slice(b", null");
}
if let Some(children) = &vnode.children {
out.extend_from_slice(b", ");
generate_vnode_children_to_bytes(ctx, children, out);
} else if vnode.patch_flag.is_some() {
out.extend_from_slice(b", null");
}
if let Some(patch_flag) = &vnode.patch_flag {
out.extend_from_slice(b", ");
out.extend_from_slice(patch_flag.bits().to_string().as_bytes());
out.extend_from_slice(b" /* ");
let mut debug = String::new();
use std::fmt::Write as _;
let _ = write!(&mut debug, "{:?}", patch_flag);
out.extend_from_slice(debug.as_bytes());
out.extend_from_slice(b" */");
}
if let Some(dynamic_props) = &vnode.dynamic_props {
out.extend_from_slice(b", ");
match dynamic_props {
DynamicProps::String(s) => {
out.extend_from_slice(s.as_bytes());
}
DynamicProps::Simple(exp) => {
out.extend_from_slice(exp.content.as_bytes());
}
}
}
out.push(b')');
if vnode.is_block {
out.push(b')');
}
}
fn generate_props_expression_to_bytes(
ctx: &CodegenContext,
props: &PropsExpression<'_>,
out: &mut Vec<u8>,
) {
match props {
PropsExpression::Object(obj) => {
out.extend_from_slice(b"{ ");
for (i, prop) in obj.properties.iter().enumerate() {
if i > 0 {
out.extend_from_slice(b", ");
}
match &prop.key {
ExpressionNode::Simple(exp) => {
let key = &exp.content;
let needs_quote = !crate::codegen::helpers::is_valid_js_identifier(key);
if needs_quote {
out.push(b'"');
out.extend_from_slice(key.as_bytes());
out.push(b'"');
} else {
out.extend_from_slice(key.as_bytes());
}
out.extend_from_slice(b": ");
}
ExpressionNode::Compound(_) => out.extend_from_slice(b"null: "),
}
generate_js_child_node_to_bytes(ctx, &prop.value, out);
}
out.extend_from_slice(b" }");
}
PropsExpression::Simple(exp) => {
if exp.is_static {
out.push(b'"');
out.extend_from_slice(exp.content.as_bytes());
out.push(b'"');
} else {
out.extend_from_slice(exp.content.as_bytes());
}
}
PropsExpression::Call(_) => out.extend_from_slice(b"null"),
}
}
fn generate_vnode_children_to_bytes(
_ctx: &CodegenContext,
children: &VNodeChildren<'_>,
out: &mut Vec<u8>,
) {
match children {
VNodeChildren::Single(text_child) => match text_child {
TemplateTextChildNode::Text(text) => {
out.push(b'"');
out.extend_from_slice(escape_js_string(&text.content).as_bytes());
out.push(b'"');
}
TemplateTextChildNode::Interpolation(_) => out.extend_from_slice(b"null"),
TemplateTextChildNode::Compound(_) => out.extend_from_slice(b"null"),
},
VNodeChildren::Simple(exp) => {
if exp.is_static {
out.push(b'"');
out.extend_from_slice(escape_js_string(&exp.content).as_bytes());
out.push(b'"');
} else {
out.extend_from_slice(exp.content.as_bytes());
}
}
_ => out.extend_from_slice(b"null"),
}
}
fn generate_assets(ctx: &mut CodegenContext, root: &RootNode<'_>) {
let mut has_resolved_assets = false;
for component in root.components.iter() {
if ctx.is_component_in_bindings(component) {
continue;
}
if helpers::is_builtin_component(component).is_some() {
continue;
}
if component == "component" {
continue;
}
ctx.use_helper(RuntimeHelper::ResolveComponent);
ctx.push("const _component_");
ctx.push(&component.replace('-', "_"));
ctx.push(" = ");
ctx.push(ctx.helper(RuntimeHelper::ResolveComponent));
ctx.push("(\"");
ctx.push(component);
ctx.push("\")");
ctx.newline();
has_resolved_assets = true;
}
for directive in root.directives.iter() {
ctx.use_helper(RuntimeHelper::ResolveDirective);
ctx.push("const _directive_");
ctx.push(&directive.replace('-', "_"));
ctx.push(" = ");
ctx.push(ctx.helper(RuntimeHelper::ResolveDirective));
ctx.push("(\"");
ctx.push(directive);
ctx.push("\")");
ctx.newline();
has_resolved_assets = true;
}
if has_resolved_assets {
ctx.newline();
}
}
#[cfg(test)]
mod tests {
use crate::{assert_codegen, compile};
#[test]
fn test_codegen_simple_element() {
assert_codegen!("<div>hello</div>" => contains: [
"_createElementBlock",
"\"div\"",
"\"hello\""
]);
}
#[test]
fn test_codegen_interpolation() {
assert_codegen!("<div>{{ msg }}</div>" => contains: [
"_toDisplayString",
"msg"
]);
}
#[test]
fn test_codegen_with_props() {
assert_codegen!(r#"<div id="app" class="container"></div>"# => contains: [
"id: \"app\"",
"class: \"container\""
]);
}
#[test]
fn test_codegen_component() {
assert_codegen!("<MyComponent />" => contains: [
"_resolveComponent",
"_createBlock",
"_component_MyComponent"
]);
}
#[test]
fn test_codegen_preamble_module() {
use crate::options::CodegenMode;
let options = super::CodegenOptions {
mode: CodegenMode::Module,
..Default::default()
};
let result = compile!("<div>hello</div>", options);
assert!(result.preamble.contains("import {"));
assert!(result.preamble.contains("from \"vue\""));
}
#[test]
fn test_codegen_v_model_on_component() {
assert_codegen!(r#"<MyComponent v-model="msg" />"# => contains: [
"_createBlock",
"_component_MyComponent",
"modelValue:",
"msg",
"\"onUpdate:modelValue\":"
]);
}
#[test]
fn test_codegen_v_model_with_arg() {
assert_codegen!(r#"<MyComponent v-model:title="pageTitle" />"# => contains: [
"title:",
"pageTitle",
"\"onUpdate:title\":"
]);
}
#[test]
fn test_codegen_v_model_on_input() {
assert_codegen!(r#"<input v-model="inputValue" />"# => contains: [
"_withDirectives",
"_vModelText",
"inputValue",
"\"onUpdate:modelValue\":"
]);
}
#[test]
fn test_codegen_v_model_with_other_props() {
let result = compile!(r#"<MonacoEditor v-model="source" :language="editorLanguage" />"#);
assert!(
!result.code.contains("/* v-model */"),
"Should not contain v-model comment"
);
assert!(
result.code.contains("modelValue:"),
"Should have modelValue prop"
);
assert!(
result.code.contains("\"onUpdate:modelValue\":"),
"Should have onUpdate:modelValue prop"
);
assert!(
result.code.contains("language:"),
"Should have language prop"
);
}
#[test]
fn test_codegen_slot_fallback() {
assert_codegen!(r#"<slot name="label">{{ label }}</slot>"# => contains: [
"_renderSlot",
"\"label\"",
"{}"
]);
let result = compile!(r#"<slot name="label">{{ label }}</slot>"#);
assert!(
result.code.contains("() => ["),
"Should have fallback function: {}",
result.code
);
assert!(
result.code.contains("_toDisplayString"),
"Should have toDisplayString for interpolation: {}",
result.code
);
}
#[test]
fn test_codegen_slot_without_fallback() {
let result = compile!(r#"<slot name="header"></slot>"#);
assert!(
result.code.contains("_renderSlot"),
"Should have renderSlot"
);
assert!(result.code.contains("\"header\""), "Should have slot name");
assert!(
!result.code.contains("() => ["),
"Should not have fallback function for empty slot: {}",
result.code
);
}
#[test]
fn test_codegen_escape_newline_in_attribute() {
let result = compile!(
r#"<div style="
color: red;
background: blue;
"></div>"#
);
assert!(
result.code.contains("\\n"),
"Should escape newlines in attribute values. Got:\n{}",
result.code
);
assert!(
!result.code.contains("style: \"\n"),
"Should not have raw newlines in string. Got:\n{}",
result.code
);
}
#[test]
fn test_codegen_escape_special_chars_in_attribute() {
let result = compile!(r#"<div data-value="line1\nline2"></div>"#);
assert!(
result.code.contains(r#"\\n"#),
"Should escape backslashes in attribute values. Got:\n{}",
result.code
);
}
#[test]
fn test_codegen_escape_multiline_style_attribute() {
let result = compile!(
r#"<div style="
display: flex;
flex-direction: column;
"></div>"#
);
assert!(
result.code.contains("style:"),
"Should have style property. Got:\n{}",
result.code
);
let style_start = result.code.find("style:").unwrap_or(0);
let code_after_style = &result.code[style_start..];
if let Some(quote_pos) = code_after_style.find('"') {
let remaining = &code_after_style[quote_pos + 1..];
if let Some(end_quote) = remaining.find('"') {
let style_value = &remaining[..end_quote];
assert!(
!style_value.contains('\n'),
"Style value should not contain raw newlines. Got:\n{}",
style_value
);
}
}
}
}