#![allow(clippy::collapsible_match)]
#![cfg_attr(test, allow(clippy::disallowed_macros))]
pub mod codegen;
pub mod errors;
pub mod options;
pub mod steps;
pub use codegen::{SsrCodegenContext, SsrCodegenResult};
pub use errors::SsrErrorCode;
pub use options::SsrCompilerOptions;
pub use steps::{
get_v_html_exp, get_v_model_exp, get_v_show_exp, get_v_text_exp, has_v_html, has_v_model,
has_v_show, has_v_text,
};
pub use vize_atelier_core::{
Allocator, CompilerError, Namespace, RootNode, RuntimeHelper, TemplateChildNode,
codegen as core_codegen, errors as core_errors, lane, parser, runtime_helpers, tokenizer,
transform,
};
use vize_atelier_core::{
lane::{transform as do_transform, transform_with_template_syntax_quirks},
options::{ParserOptions, TemplateSyntaxMode, TransformOptions},
parser::parse_with_options_and_template_syntax,
};
use vize_carton::{Bump, String, profile};
pub fn compile_ssr<'a>(
allocator: &'a Bump,
source: &'a str,
) -> (RootNode<'a>, Vec<CompilerError>, SsrCodegenResult) {
compile_ssr_with_options(allocator, source, SsrCompilerOptions::default())
}
pub fn compile_ssr_with_options<'a>(
allocator: &'a Bump,
source: &'a str,
options: SsrCompilerOptions,
) -> (RootNode<'a>, Vec<CompilerError>, SsrCodegenResult) {
compile_ssr_inner(allocator, source, options, TemplateSyntaxMode::Standard)
}
#[deprecated(note = "use compile_ssr_with_template_syntax instead")]
pub fn compile_ssr_with_vue_parser_quirks<'a>(
allocator: &'a Bump,
source: &'a str,
options: SsrCompilerOptions,
) -> (RootNode<'a>, Vec<CompilerError>, SsrCodegenResult) {
compile_ssr_inner(allocator, source, options, TemplateSyntaxMode::Quirks)
}
#[doc(hidden)]
pub fn compile_ssr_with_template_syntax<'a>(
allocator: &'a Bump,
source: &'a str,
options: SsrCompilerOptions,
template_syntax: TemplateSyntaxMode,
) -> (RootNode<'a>, Vec<CompilerError>, SsrCodegenResult) {
compile_ssr_inner(allocator, source, options, template_syntax)
}
fn compile_ssr_inner<'a>(
allocator: &'a Bump,
source: &'a str,
options: SsrCompilerOptions,
template_syntax: TemplateSyntaxMode,
) -> (RootNode<'a>, Vec<CompilerError>, SsrCodegenResult) {
let codegen_options = options.clone();
let parser_opts = ParserOptions {
is_void_tag: vize_carton::is_void_tag,
is_native_tag: Some(vize_carton::is_native_tag),
custom_renderer: options.custom_renderer,
is_pre_tag: |tag| tag == "pre",
get_namespace,
comments: options.comments,
dialect: options.dialect,
..ParserOptions::default()
};
let (mut root, errors) = profile!(
"atelier.ssr.template.parse",
parse_with_options_and_template_syntax(allocator, source, parser_opts, template_syntax)
);
let fatal_count = errors.iter().filter(|e| !e.is_recoverable()).count();
if fatal_count > 0 {
let codegen_result = SsrCodegenResult {
code: String::default(),
preamble: String::default(),
};
return (root, errors.to_vec(), codegen_result);
}
let transform_opts = TransformOptions {
prefix_identifiers: true, hoist_static: false, cache_handlers: false, scope_id: codegen_options.scope_id.clone(),
ssr: true,
is_ts: codegen_options.is_ts,
inline: codegen_options.inline,
custom_renderer: codegen_options.custom_renderer,
binding_metadata: codegen_options.binding_metadata.clone(),
dialect: codegen_options.dialect,
..Default::default()
};
let analysis = options.croquis.map(|c| &*allocator.alloc(*c));
let template_syntax_quirks = template_syntax.is_quirks();
let transform_errors = profile!(
"atelier.ssr.template.transform",
if template_syntax_quirks {
transform_with_template_syntax_quirks(allocator, &mut root, transform_opts, analysis)
} else {
do_transform(allocator, &mut root, transform_opts, analysis)
}
);
let mut errors = errors.to_vec();
errors.extend(transform_errors);
let codegen_ctx = SsrCodegenContext::new(allocator, &codegen_options);
let codegen_result = profile!("atelier.ssr.template.codegen", codegen_ctx.generate(&root));
(root, errors, codegen_result)
}
fn get_namespace(tag: &str, parent: Option<&str>) -> Namespace {
if vize_carton::is_svg_tag(tag) {
return Namespace::Svg;
}
if vize_carton::is_math_ml_tag(tag) {
return Namespace::MathMl;
}
if let Some(parent_tag) = parent {
if vize_carton::is_svg_tag(parent_tag) && tag != "foreignObject" {
return Namespace::Svg;
}
if vize_carton::is_math_ml_tag(parent_tag)
&& tag != "annotation-xml"
&& tag != "foreignObject"
{
return Namespace::MathMl;
}
}
Namespace::Html
}
#[cfg(test)]
mod tests {
use super::{
Bump, SsrCompilerOptions, compile_ssr, compile_ssr_with_options,
compile_ssr_with_template_syntax,
};
use vize_atelier_core::TemplateSyntaxMode;
#[test]
fn test_compile_simple_element() {
let allocator = Bump::new();
let (root, errors, result) = compile_ssr(&allocator, "<div>hello</div>");
assert!(errors.is_empty());
assert_eq!(root.children.len(), 1);
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_compile_interpolation() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(&allocator, "<div>{{ msg }}</div>");
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_scoped_dynamic_component_keeps_scope_id() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr_with_options(
&allocator,
r#"<component :is="tag"><span>Logo</span></component>"#,
SsrCompilerOptions {
scope_id: Some("data-v-test".into()),
..SsrCompilerOptions::default()
},
);
assert!(errors.is_empty());
assert!(
result
.code
.contains(r#"_mergeProps({ }, { "data-v-test": "" })"#)
|| result.code.contains(r#"{ "data-v-test": "" }"#),
"{}",
result.code
);
assert!(
result
.code
.contains(r#"_createElementVNode("span", { "data-v-test": "" }"#),
"{}",
result.code
);
}
#[test]
fn test_scoped_component_keeps_scope_id() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr_with_options(
&allocator,
r#"<NuxtLink to="/news" class="news__link"><span>News</span></NuxtLink>"#,
SsrCompilerOptions {
scope_id: Some("data-v-news".into()),
..SsrCompilerOptions::default()
},
);
assert!(errors.is_empty());
assert!(
result.code.contains(r#""data-v-news": """#),
"{}",
result.code
);
assert!(
result.code.contains(r#"class: "news__link""#),
"{}",
result.code
);
}
#[test]
fn test_compile_template_syntax_quirks_accepts_invalid_html_self_closing() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr_with_template_syntax(
&allocator,
"<div /><span></span>",
Default::default(),
TemplateSyntaxMode::Quirks,
);
assert!(errors.is_empty(), "Errors: {:?}", errors);
assert!(!result.code.is_empty());
}
#[test]
fn test_compile_standard_warns_and_rewrites_invalid_html_self_closing() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(&allocator, "<div /><span></span>");
assert!(errors.iter().any(|error| error.is_recoverable()));
assert!(!result.code.is_empty());
}
#[test]
fn test_compile_strict_rejects_invalid_html_self_closing() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr_with_template_syntax(
&allocator,
"<div /><span></span>",
Default::default(),
TemplateSyntaxMode::Strict,
);
assert!(errors.iter().any(|error| !error.is_recoverable()));
assert!(result.code.is_empty());
}
#[test]
fn test_ssr_v_model_textarea_renders_bound_value() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(&allocator, r#"<textarea v-model="x"></textarea>"#);
assert!(errors.is_empty(), "{errors:?}");
assert!(
result.code.contains("_ssrInterpolate(_ctx.x)"),
"expected textarea body to interpolate the model value, got:\n{}",
result.code
);
}
#[test]
fn test_ssr_v_model_select_marks_matching_option_selected() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<select v-model="x"><option value="a">A</option><option value="b">B</option></select>"#,
);
assert!(errors.is_empty(), "{errors:?}");
assert!(
result.code.contains("_ssrLooseEqual(_ctx.x, \"a\")"),
"expected loose-equal for option a, got:\n{}",
result.code
);
assert!(
result.code.contains("_ssrLooseEqual(_ctx.x, \"b\")"),
"expected loose-equal for option b, got:\n{}",
result.code
);
assert!(
result.code.contains("\" selected\""),
"expected ` selected` literal, got:\n{}",
result.code
);
}
#[test]
fn test_dynamic_slot_outlet_name_stays_expression() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr_with_options(
&allocator,
r#"<Parent><slot :name="((item.slot || 'item') as keyof Slots)" :item="item" /></Parent>"#,
SsrCompilerOptions {
is_ts: true,
..SsrCompilerOptions::default()
},
);
assert!(errors.is_empty());
assert!(
result
.code
.contains(r#"_ssrRenderSlot(_ctx.$slots, _ctx.item.slot || "item""#),
"{}",
result.code
);
assert!(
result
.code
.contains(r#"_renderSlot(_ctx.$slots, _ctx.item.slot || "item""#),
"{}",
result.code
);
}
#[test]
fn test_ssr_v_if_v_else() {
let allocator = Bump::new();
let (_, errors, result) =
compile_ssr(&allocator, r#"<div v-if="ok">yes</div><p v-else>no</p>"#);
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_v_for_list() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<ul><li v-for="item in items" :key="item.id">{{ item.name }}</li></ul>"#,
);
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_static_and_dynamic_attrs() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<a class="link" :href="url" target="_blank">{{ label }}</a>"#,
);
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_v_bind_object() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(&allocator, r#"<div v-bind="attrs">content</div>"#);
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_v_html() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(&allocator, r#"<div v-html="raw"></div>"#);
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_dynamic_class_and_style() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<div :class="{ active: isActive }" :style="{ color }">x</div>"#,
);
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_component_with_props_and_slot() {
let allocator = Bump::new();
let (_, errors, result) =
compile_ssr(&allocator, r#"<MyCard :title="t"><p>body</p></MyCard>"#);
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_fragment_multiple_roots() {
let allocator = Bump::new();
let (_, errors, result) =
compile_ssr(&allocator, r#"<header>a</header><main>{{ b }}</main>"#);
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_text_and_interpolation_mix() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<p>Hello {{ name }}, you have {{ count }} items</p>"#,
);
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_v_show() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(&allocator, r#"<div v-show="visible">toggle</div>"#);
assert!(errors.is_empty());
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_dynamic_v_for_slot_uses_create_slots() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<Child>
<template v-for="(_, name) in slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
<template #trailing="{ ui }">
<div>{{ ui }}</div>
</template>
</Child>"#,
);
assert!(errors.is_empty());
assert!(
result.code.contains("_createSlots("),
"expected createSlots for dynamic v-for slot:\n{}",
result.code
);
assert!(
result.code.contains("name,") || result.code.contains("name: name"),
"expected local `name` alias in looped slot entry:\n{}",
result.code
);
assert!(
!result.code.contains("_ctx.slotData"),
"scoped slot param `slotData` must not leak as `_ctx.slotData`:\n{}",
result.code
);
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_slot_outlet_fallback_survives_vnode_branch() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<Outer>
<button>
<slot :ui="ui">
<span v-if="label">{{ label }}</span>
</slot>
</button>
</Outer>"#,
);
assert!(errors.is_empty());
assert!(
result
.code
.contains("_renderSlot(_ctx.$slots, \"default\", { ui: _ctx.ui }, () => ["),
"vnode branch must pass the slot fallback:\n{}",
result.code
);
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_conditional_slot_uses_create_slots() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<Child>
<template v-if="ok" #header>
<span>head</span>
</template>
</Child>"#,
);
assert!(errors.is_empty());
assert!(
result.code.contains("_createSlots("),
"expected createSlots for conditional slot:\n{}",
result.code
);
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_component_level_v_slot_binds_props() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<Comp v-slot="{ item }">
<span>{{ item.label }}</span>
</Comp>"#,
);
assert!(errors.is_empty());
assert!(
result.code.contains("default: _withCtx(({ item }"),
"component-level v-slot must bind its props pattern:\n{}",
result.code
);
assert!(
!result.code.contains("_ctx.item"),
"scoped slot param `item` must not leak as `_ctx.item`:\n{}",
result.code
);
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_named_scoped_slot_keeps_props_in_vnode_fallback() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<Outer>
<Inner>
<template #header="{ collapsed }">
<span>{{ collapsed }}</span>
</template>
<template #default="{ collapsed }">
<Leaf :collapsed="collapsed" />
</template>
</Inner>
</Outer>"#,
);
assert!(errors.is_empty());
assert!(
result.code.contains("header: _withCtx(({ collapsed })"),
"vnode fallback must bind the header slot props:\n{}",
result.code
);
assert!(
!result.code.contains("_ctx.collapsed"),
"scoped slot param `collapsed` must not leak as `_ctx.collapsed`:\n{}",
result.code
);
insta::assert_snapshot!(result.code.as_str());
}
#[test]
fn test_ssr_dynamic_slot_vnode_fallback_uses_create_slots() {
let allocator = Bump::new();
let (_, errors, result) = compile_ssr(
&allocator,
r#"<Outer>
<Inner>
<template v-for="(_, name) in slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
<template #trailing>x</template>
</Inner>
</Outer>"#,
);
assert!(errors.is_empty());
assert!(
result.code.matches("_createSlots(").count() >= 2,
"expected createSlots in both push and vnode fallback branches:\n{}",
result.code
);
assert!(
!result.code.contains("_ctx.slotData"),
"scoped slot param must not leak as `_ctx.slotData`:\n{}",
result.code
);
insta::assert_snapshot!(result.code.as_str());
}
}