use crate::compile_script::{compile_script_setup_inline, TemplateParts};
use crate::compile_template::{
compile_template_block, compile_template_block_vapor, extract_template_parts,
extract_template_parts_full,
};
use crate::rewrite_default::rewrite_default;
use crate::script::ScriptCompileContext;
use crate::types::*;
pub use crate::compile_script::ScriptCompileResult;
pub fn compile_sfc(
descriptor: &SfcDescriptor,
options: SfcCompileOptions,
) -> Result<SfcCompileResult, SfcError> {
let mut errors = Vec::new();
let mut warnings = Vec::new();
let mut code = String::new();
let mut css = None;
let filename = options.script.id.as_deref().unwrap_or("anonymous.vue");
let scope_id = generate_scope_id(filename);
let has_scoped = descriptor.styles.iter().any(|s| s.scoped);
let is_vapor = descriptor
.script_setup
.as_ref()
.map(|s| s.attrs.contains_key("vapor"))
.unwrap_or(false)
|| descriptor
.script
.as_ref()
.map(|s| s.attrs.contains_key("vapor"))
.unwrap_or(false);
let source_has_ts = descriptor
.script_setup
.as_ref()
.and_then(|s| s.lang.as_ref())
.is_some_and(|l| l == "ts" || l == "tsx")
|| descriptor
.script
.as_ref()
.and_then(|s| s.lang.as_ref())
.is_some_and(|l| l == "ts" || l == "tsx");
let is_ts = options.script.is_ts || options.template.is_ts || source_has_ts;
let component_name = extract_component_name(filename);
let has_script_setup = descriptor.script_setup.is_some();
let has_script = descriptor.script.is_some();
let has_template = descriptor.template.is_some();
if !has_script && !has_script_setup && has_template {
let template = descriptor.template.as_ref().unwrap();
let mut template_opts = options.template.clone();
let mut dom_opts = template_opts.compiler_options.take().unwrap_or_default();
dom_opts.hoist_static = true;
template_opts.compiler_options = Some(dom_opts);
let template_result = compile_template_block(
template,
&template_opts,
&scope_id,
has_scoped,
is_ts,
None,
None,
);
match template_result {
Ok(template_code) => {
let wrapped = template_code.replace("export function render(", "function render(");
let mut output = String::with_capacity(wrapped.len() + 128);
output.push_str(&wrapped);
output.push_str("\nconst _sfc_main = {};\n");
output.push_str("_sfc_main.render = render;\n");
output.push_str("export default _sfc_main;\n");
code = output;
}
Err(e) => errors.push(e),
}
let all_css = compile_styles(&descriptor.styles, &scope_id, &options.style, &mut warnings);
if !all_css.is_empty() {
css = Some(all_css);
}
return Ok(SfcCompileResult {
code,
css,
map: None,
errors,
warnings,
bindings: None,
});
}
if has_script && !has_script_setup {
let script = descriptor.script.as_ref().unwrap();
let source_is_ts = script
.lang
.as_ref()
.is_some_and(|l| l == "ts" || l == "tsx");
let (rewritten_script, _has_default) =
rewrite_default(&script.content, "_sfc_main", source_is_ts);
let final_script = if source_is_ts && !is_ts {
crate::compile_script::typescript::transform_typescript_to_js(&rewritten_script)
} else {
rewritten_script
};
if has_template {
let template = descriptor.template.as_ref().unwrap();
let mut template_opts = options.template.clone();
let mut dom_opts = template_opts.compiler_options.take().unwrap_or_default();
dom_opts.hoist_static = true;
template_opts.compiler_options = Some(dom_opts);
let template_result = compile_template_block(
template,
&template_opts,
&scope_id,
has_scoped,
is_ts,
None, None, );
match template_result {
Ok(template_code) => {
let (template_imports, template_hoisted, render_fn) =
extract_template_parts_full(&template_code);
code.push_str(&template_imports);
if !template_imports.is_empty() {
code.push('\n');
}
code.push_str(&final_script);
code.push('\n');
if !template_hoisted.is_empty() {
code.push_str(&template_hoisted);
code.push('\n');
}
code.push_str(&render_fn);
code.push('\n');
code.push_str("_sfc_main.render = render\n");
code.push_str("export default _sfc_main\n");
}
Err(e) => {
errors.push(e);
code = script.content.to_string();
code.push('\n');
}
}
} else {
code.push_str(&final_script);
code.push_str("\nexport default _sfc_main\n");
}
let all_css = compile_styles(&descriptor.styles, &scope_id, &options.style, &mut warnings);
if !all_css.is_empty() {
css = Some(all_css);
}
return Ok(SfcCompileResult {
code,
css,
map: None,
errors,
warnings,
bindings: None,
});
}
let script_setup = match descriptor.script_setup.as_ref() {
Some(s) => s,
None => {
return Err(SfcError {
message:
"At least one <template> or <script> is required in a single file component."
.to_string(),
code: None,
loc: None,
});
}
};
let normal_script_content = if has_script {
let script = descriptor.script.as_ref().unwrap();
let source_is_ts = script
.lang
.as_ref()
.is_some_and(|l| l == "ts" || l == "tsx");
Some(extract_normal_script_content(
&script.content,
source_is_ts,
is_ts,
))
} else {
None
};
let croquis = crate::script::analyze_script_setup_to_summary(&script_setup.content);
let mut script_bindings = croquis_to_legacy_bindings(&croquis.bindings);
let mut ctx = ScriptCompileContext::new(&script_setup.content);
ctx.analyze();
for (name, bt) in &ctx.bindings.bindings {
if matches!(bt, BindingType::Props | BindingType::PropsAliased) {
script_bindings.bindings.entry(name.clone()).or_insert(*bt);
}
}
if has_script {
let script = descriptor.script.as_ref().unwrap();
let normal_ctx = ScriptCompileContext::new(&script.content);
for line in script.content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("export const ") || trimmed.starts_with("export let ") {
let rest = if let Some(r) = trimmed.strip_prefix("export const ") {
r
} else {
trimmed.strip_prefix("export let ").unwrap()
};
if let Some(name_end) =
rest.find(|c: char| c == '=' || c == ':' || c.is_whitespace())
{
let name = rest[..name_end].trim();
if !name.is_empty() && !name.starts_with('{') && !name.starts_with('[') {
script_bindings
.bindings
.insert(name.to_string(), BindingType::SetupConst);
}
}
}
}
drop(normal_ctx);
}
let template_result = if let Some(template) = &descriptor.template {
if is_vapor {
Some(compile_template_block_vapor(
template, &scope_id, has_scoped,
))
} else {
Some(compile_template_block(
template,
&options.template,
&scope_id,
has_scoped,
is_ts,
Some(&script_bindings), Some(croquis), ))
}
} else {
None
};
let (template_imports, template_hoisted, template_preamble, render_body) =
match &template_result {
Some(Ok(template_code)) => extract_template_parts(template_code),
Some(Err(e)) => {
errors.push(e.clone());
(String::new(), String::new(), String::new(), String::new())
}
None => (String::new(), String::new(), String::new(), String::new()),
};
let source_is_ts = script_setup
.lang
.as_ref()
.is_some_and(|l| l == "ts" || l == "tsx");
let script_result = compile_script_setup_inline(
&script_setup.content,
&component_name,
is_ts,
source_is_ts,
TemplateParts {
imports: &template_imports,
hoisted: &template_hoisted,
preamble: &template_preamble,
render_body: &render_body,
},
normal_script_content.as_deref(),
)?;
code.push_str(&script_result.code);
let all_css = compile_styles(&descriptor.styles, &scope_id, &options.style, &mut warnings);
if !all_css.is_empty() {
css = Some(all_css);
}
Ok(SfcCompileResult {
code,
css,
map: None,
errors,
warnings,
bindings: script_result.bindings,
})
}
fn compile_styles(
styles: &[SfcStyleBlock],
scope_id: &str,
base_opts: &StyleCompileOptions,
warnings: &mut Vec<SfcError>,
) -> String {
let mut all_css = String::new();
for style in styles {
let style_opts = StyleCompileOptions {
id: {
let mut id = String::with_capacity(scope_id.len() + 7);
id.push_str("data-v-");
id.push_str(scope_id);
id
},
scoped: style.scoped,
..base_opts.clone()
};
match crate::style::compile_style(style, &style_opts) {
Ok(style_css) => {
if !all_css.is_empty() {
all_css.push('\n');
}
all_css.push_str(&style_css);
}
Err(e) => warnings.push(e),
}
}
all_css
}
fn generate_scope_id(filename: &str) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
filename.hash(&mut hasher);
let value = hasher.finish() & 0xFFFFFFFF;
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(8);
for shift in (0..32).step_by(4).rev() {
let digit = ((value >> shift) & 0xF) as usize;
out.push(HEX[digit] as char);
}
out
}
fn extract_component_name(filename: &str) -> String {
std::path::Path::new(filename)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("anonymous")
.to_string()
}
fn extract_normal_script_content(content: &str, source_is_ts: bool, output_is_ts: bool) -> String {
use oxc_allocator::Allocator;
use oxc_ast::ast::Statement;
use oxc_codegen::Codegen;
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::{GetSpan, SourceType};
use oxc_transformer::{TransformOptions, Transformer, TypeScriptOptions};
let source_type = if source_is_ts {
SourceType::ts()
} else {
SourceType::mjs()
};
let allocator = Allocator::default();
let ret = Parser::new(&allocator, content, source_type).parse();
if !ret.errors.is_empty() {
return content
.lines()
.filter(|line| !line.trim().starts_with("export default"))
.collect::<Vec<_>>()
.join("\n");
}
let program = ret.program;
let mut output = String::new();
let mut last_end = 0;
let mut skip_spans: Vec<(u32, u32)> = Vec::new();
let mut rewrites: Vec<(u32, u32, String)> = Vec::new();
for stmt in program.body.iter() {
match stmt {
Statement::ExportDefaultDeclaration(decl) => {
let stmt_start = stmt.span().start;
let stmt_end = stmt.span().end;
let stmt_text = &content[stmt_start as usize..stmt_end as usize];
let rewritten = stmt_text.replacen("export default", "const __default__ =", 1);
rewrites.push((stmt_start, stmt_end, rewritten));
let _ = decl; }
Statement::ExportNamedDeclaration(decl) => {
let has_default_export = decl.specifiers.iter().any(|s| {
matches!(&s.exported, oxc_ast::ast::ModuleExportName::IdentifierName(name) if name.name == "default")
|| matches!(&s.exported, oxc_ast::ast::ModuleExportName::IdentifierReference(name) if name.name == "default")
});
if has_default_export {
skip_spans.push((stmt.span().start, stmt.span().end));
}
}
_ => {}
}
}
let mut modifications: Vec<(u32, u32, Option<String>)> = Vec::new();
for (start, end, replacement) in rewrites {
modifications.push((start, end, Some(replacement)));
}
for (start, end) in &skip_spans {
modifications.push((*start, *end, None));
}
modifications.sort_by_key(|m| m.0);
for (start, end, replacement) in &modifications {
output.push_str(&content[last_end..*start as usize]);
if let Some(repl) = replacement {
output.push_str(repl);
}
last_end = *end as usize;
}
if last_end < content.len() {
output.push_str(&content[last_end..]);
}
let extracted = output.trim().to_string();
if source_is_ts && !output_is_ts {
let allocator2 = Allocator::default();
let ret2 = Parser::new(&allocator2, &extracted, SourceType::ts()).parse();
if ret2.errors.is_empty() {
let mut program2 = ret2.program;
let semantic_ret = SemanticBuilder::new().build(&program2);
if semantic_ret.errors.is_empty() {
let scoping = semantic_ret.semantic.into_scoping();
let transform_options = TransformOptions {
typescript: TypeScriptOptions {
only_remove_type_imports: true,
..Default::default()
},
..Default::default()
};
let transform_ret =
Transformer::new(&allocator2, std::path::Path::new(""), &transform_options)
.build_with_scoping(scoping, &mut program2);
if transform_ret.errors.is_empty() {
return Codegen::new().build(&program2).code;
}
}
}
}
extracted
}
fn croquis_to_legacy_bindings(src: &vize_croquis::analysis::BindingMetadata) -> BindingMetadata {
let mut dst = BindingMetadata::default();
dst.is_script_setup = src.is_script_setup;
for (name, bt) in src.iter() {
dst.bindings.insert(name.to_string(), bt);
}
for (local, key) in &src.props_aliases {
dst.props_aliases.insert(local.to_string(), key.to_string());
}
dst
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{parse_sfc, SfcParseOptions};
#[test]
fn test_generate_scope_id() {
let id = generate_scope_id("src/App.vue");
assert_eq!(id.len(), 8);
}
#[test]
fn test_extract_component_name() {
assert_eq!(extract_component_name("src/App.vue"), "App");
assert_eq!(extract_component_name("MyComponent.vue"), "MyComponent");
}
#[test]
#[ignore = "TODO: fix v-model prop quoting"]
fn test_v_model_on_component_in_sfc() {
let source = r#"<script setup>
import { ref } from 'vue'
import MyComponent from './MyComponent.vue'
const msg = ref('')
</script>
<template>
<MyComponent v-model="msg" :language="'en'" />
</template>"#;
let descriptor =
parse_sfc(source, SfcParseOptions::default()).expect("Failed to parse SFC");
let opts = SfcCompileOptions::default();
let result = compile_sfc(&descriptor, opts).expect("Failed to compile SFC");
assert!(
!result.code.contains("/* v-model */"),
"Should not contain v-model comment. Got:\n{}",
result.code
);
assert!(
result.code.contains("\"modelValue\":"),
"Should have modelValue prop. Got:\n{}",
result.code
);
assert!(
result.code.contains("\"onUpdate:modelValue\":"),
"Should have onUpdate:modelValue prop. Got:\n{}",
result.code
);
}
#[test]
#[ignore = "TODO: fix inline mode ref handling"]
fn test_bindings_passed_to_template() {
let source = r#"<script setup lang="ts">
import { ref } from 'vue';
import MonacoEditor from './MonacoEditor.vue';
const selectedPreset = ref('test');
const options = ref({ ssr: false });
function handleChange(val: string) { selectedPreset.value = val; }
</script>
<template>
<div>{{ selectedPreset }}</div>
<select :value="selectedPreset" @change="handleChange($event.target.value)">
<option value="a">A</option>
</select>
<input type="checkbox" v-model="options.ssr" />
<MonacoEditor />
</template>"#;
let descriptor =
parse_sfc(source, SfcParseOptions::default()).expect("Failed to parse SFC");
let opts = SfcCompileOptions::default();
let result = compile_sfc(&descriptor, opts).expect("Failed to compile SFC");
eprintln!("=== COMPILED OUTPUT ===\n{}", result.code);
assert!(
result.code.contains("$setup.selectedPreset"),
"selectedPreset should have $setup prefix in non-inline mode with bindings. Got:\n{}",
result.code
);
assert!(
result.code.contains("$setup.handleChange"),
"handleChange should have $setup prefix in non-inline mode with bindings. Got:\n{}",
result.code
);
assert!(
result.code.contains("options"),
"options should be in __returned__. Got:\n{}",
result.code
);
assert!(
result.code.contains("$setup.options"),
"options.ssr should have $setup prefix. Got:\n{}",
result.code
);
assert!(
result.code.contains("MonacoEditor"),
"MonacoEditor should be in __returned__. Got:\n{}",
result.code
);
}
#[test]
#[ignore = "TODO: fix nested v-if prefix"]
fn test_nested_v_if_no_double_prefix() {
let source = r#"<script setup lang="ts">
import { ref } from 'vue';
import CodeHighlight from './CodeHighlight.vue';
const output = ref(null);
</script>
<template>
<div v-if="output">
<div v-if="output.preamble" class="preamble">
<CodeHighlight :code="output.preamble" />
</div>
</div>
</template>"#;
let descriptor =
parse_sfc(source, SfcParseOptions::default()).expect("Failed to parse SFC");
let opts = SfcCompileOptions::default();
let result = compile_sfc(&descriptor, opts).expect("Failed to compile SFC");
eprintln!("=== NESTED V-IF OUTPUT ===\n{}", result.code);
assert!(
!result.code.contains("$setup.$setup"),
"Should NOT have double $setup prefix. Got:\n{}",
result.code
);
assert!(
result.code.contains("$setup.output"),
"Should have single $setup prefix for output. Got:\n{}",
result.code
);
assert!(
result.code.contains("CodeHighlight"),
"Should contain CodeHighlight. Got:\n{}",
result.code
);
}
#[test]
fn test_typescript_preserved_in_event_handler() {
let source = r#"<script setup lang="ts">
type PresetKey = 'a' | 'b'
function handlePresetChange(key: PresetKey) {}
</script>
<template>
<select @change="handlePresetChange(($event.target).value)">
<option value="a">A</option>
</select>
</template>"#;
let descriptor =
parse_sfc(source, SfcParseOptions::default()).expect("Failed to parse SFC");
let opts = SfcCompileOptions {
script: ScriptCompileOptions {
is_ts: true,
..Default::default()
},
..Default::default()
};
let result = compile_sfc(&descriptor, opts).expect("Failed to compile SFC");
eprintln!("TypeScript SFC output:\n{}", result.code);
assert!(
result.code.contains("type PresetKey"),
"Should preserve type alias with lang='ts'. Got:\n{}",
result.code
);
assert!(
result.code.contains("key: PresetKey"),
"Should preserve function parameter type with lang='ts'. Got:\n{}",
result.code
);
assert!(
result.code.contains("handlePresetChange"),
"Should have event handler. Got:\n{}",
result.code
);
}
#[test]
fn test_typescript_function_types_preserved() {
let source = r#"<script setup lang="ts">
interface Item {
id: number;
name: string;
}
const getNumberOfItems = (
items: Item[]
): string => {
return items.length.toString();
};
const foo: string = "bar";
const count: number = 42;
function processData(data: Record<string, unknown>): void {
console.log(data);
}
</script>
<template>
<div>{{ foo }}</div>
</template>"#;
let descriptor =
parse_sfc(source, SfcParseOptions::default()).expect("Failed to parse SFC");
let opts = SfcCompileOptions {
script: ScriptCompileOptions {
is_ts: true,
..Default::default()
},
..Default::default()
};
let result = compile_sfc(&descriptor, opts).expect("Failed to compile SFC");
eprintln!("TypeScript function types output:\n{}", result.code);
assert!(
result.code.contains("interface Item"),
"Should preserve interface with lang='ts'. Got:\n{}",
result.code
);
assert!(
result.code.contains(": Item[]"),
"Should preserve array type annotation with lang='ts'. Got:\n{}",
result.code
);
assert!(
result.code.contains("foo"),
"Should have variable foo. Got:\n{}",
result.code
);
}
#[test]
fn test_full_sfc_props_destructure() {
let input = r#"<script setup lang="ts">
import { computed } from 'vue'
const {
name,
count = 0,
} = defineProps<{
name: string
count?: number
}>()
const doubled = computed(() => count * 2)
</script>
<template>
<div class="card">
<h2>{{ name }}</h2>
<p>Count: {{ count }} (doubled: {{ doubled }})</p>
</div>
</template>"#;
let parse_opts = SfcParseOptions::default();
let descriptor = parse_sfc(input, parse_opts).unwrap();
let mut compile_opts = SfcCompileOptions::default();
compile_opts.script.id = Some("test.vue".to_string());
let result = compile_sfc(&descriptor, compile_opts).unwrap();
eprintln!("=== Full SFC props destructure output ===\n{}", result.code);
assert!(
result.code.contains("__props.name") || result.code.contains("name"),
"Should have name access. Got:\n{}",
result.code
);
}
#[test]
fn test_let_var_unref() {
let input = r#"
<script setup>
const a = 1
let b = 2
var c = 3
</script>
<template>
<div>{{ a }} {{ b }} {{ c }}</div>
</template>
"#;
let parse_opts = SfcParseOptions::default();
let descriptor = parse_sfc(input, parse_opts).unwrap();
let mut compile_opts = SfcCompileOptions::default();
compile_opts.script.id = Some("test.vue".to_string());
let result = compile_sfc(&descriptor, compile_opts).unwrap();
eprintln!("Let/var unref test output:\n{}", result.code);
if let Some(bindings) = &result.bindings {
eprintln!("Bindings:");
for (name, binding_type) in &bindings.bindings {
eprintln!(" {} => {:?}", name, binding_type);
}
assert!(
matches!(bindings.bindings.get("a"), Some(BindingType::LiteralConst)),
"a should be LiteralConst"
);
assert!(
matches!(bindings.bindings.get("b"), Some(BindingType::SetupLet)),
"b should be SetupLet"
);
assert!(
matches!(bindings.bindings.get("c"), Some(BindingType::SetupLet)),
"c should be SetupLet"
);
}
assert!(
result.code.contains("unref as _unref"),
"Should import _unref. Got:\n{}",
result.code
);
assert!(
result.code.contains("_unref(b)"),
"b should be wrapped with _unref. Got:\n{}",
result.code
);
assert!(
result.code.contains("_unref(c)"),
"c should be wrapped with _unref. Got:\n{}",
result.code
);
}
#[test]
fn test_extract_normal_script_content() {
let input = r#"import type { NuxtRoute } from "@typed-router";
import { useBreakpoint } from "./_utils";
import Button from "./Button.vue";
interface TabItem {
name: string;
label: string;
}
export default {
name: 'Tab'
}
"#;
let result = extract_normal_script_content(input, true, true);
eprintln!("Extracted normal script content (preserve TS):\n{}", result);
assert!(
result.contains("import type { NuxtRoute }"),
"Should contain type import"
);
assert!(
result.contains("import { useBreakpoint }"),
"Should contain named import"
);
assert!(
result.contains("import Button"),
"Should contain default import"
);
assert!(
result.contains("interface TabItem"),
"Should contain interface"
);
assert!(
!result.contains("export default"),
"Should NOT contain export default"
);
}
#[test]
fn test_compile_both_script_blocks() {
let source = r#"<script lang="ts">
import type { RouteLocation } from "vue-router";
interface TabItem {
name: string;
label: string;
}
export type { TabItem };
</script>
<script setup lang="ts">
const { items } = defineProps<{
items: Array<TabItem>;
}>();
</script>
<template>
<div v-for="item in items" :key="item.name">
{{ item.label }}
</div>
</template>"#;
let descriptor =
parse_sfc(source, SfcParseOptions::default()).expect("Failed to parse SFC");
eprintln!(
"Descriptor script: {:?}",
descriptor.script.as_ref().map(|s| &s.content)
);
eprintln!(
"Descriptor script_setup: {:?}",
descriptor.script_setup.as_ref().map(|s| &s.content)
);
let opts = SfcCompileOptions {
script: ScriptCompileOptions {
is_ts: true,
..Default::default()
},
template: TemplateCompileOptions {
is_ts: true,
..Default::default()
},
..Default::default()
};
let result = compile_sfc(&descriptor, opts).expect("Failed to compile SFC");
eprintln!("=== COMPILED OUTPUT ===\n{}", result.code);
assert!(
result.code.contains("RouteLocation") || result.code.contains("interface TabItem"),
"Should contain type definitions from normal script. Got:\n{}",
result.code
);
}
#[test]
fn test_define_model_basic() {
let source = r#"<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model">
</template>"#;
let descriptor =
parse_sfc(source, SfcParseOptions::default()).expect("Failed to parse SFC");
let opts = SfcCompileOptions::default();
let result = compile_sfc(&descriptor, opts).expect("Failed to compile SFC");
eprintln!("=== defineModel OUTPUT ===\n{}", result.code);
assert!(
result.code.contains("useModel as _useModel"),
"Should import useModel. Got:\n{}",
result.code
);
assert!(
result.code.contains("modelValue"),
"Should have modelValue prop. Got:\n{}",
result.code
);
assert!(
result.code.contains("update:modelValue"),
"Should have update:modelValue emit. Got:\n{}",
result.code
);
assert!(
result.code.contains("_useModel(__props"),
"Should use _useModel in setup. Got:\n{}",
result.code
);
}
#[test]
fn test_define_model_with_name() {
let source = r#"<script setup>
const title = defineModel('title')
</script>
<template>
<input v-model="title">
</template>"#;
let descriptor =
parse_sfc(source, SfcParseOptions::default()).expect("Failed to parse SFC");
let opts = SfcCompileOptions::default();
let result = compile_sfc(&descriptor, opts).expect("Failed to compile SFC");
eprintln!("=== defineModel with name OUTPUT ===\n{}", result.code);
assert!(
result.code.contains("title:") || result.code.contains("\"title\""),
"Should have title prop. Got:\n{}",
result.code
);
assert!(
result.code.contains("update:title"),
"Should have update:title emit. Got:\n{}",
result.code
);
}
#[test]
fn test_non_script_setup_typescript_preserved() {
let source = r#"<script lang="ts">
interface Props {
name: string;
count?: number;
}
export default {
name: 'MyComponent',
props: {
name: String,
count: Number
} as Props,
setup(props: Props) {
const message: string = `Hello, ${props.name}!`;
return { message };
}
}
</script>
<template>
<div>{{ message }}</div>
</template>"#;
let descriptor =
parse_sfc(source, SfcParseOptions::default()).expect("Failed to parse SFC");
let opts = SfcCompileOptions {
script: ScriptCompileOptions {
is_ts: true,
..Default::default()
},
..Default::default()
};
let result = compile_sfc(&descriptor, opts).expect("Failed to compile SFC");
eprintln!("=== Non-script-setup TS output ===\n{}", result.code);
assert!(
result.code.contains("interface Props") || result.code.contains(": Props"),
"Should preserve TypeScript with is_ts=true. Got:\n{}",
result.code
);
assert!(
result.code.contains("name: 'MyComponent'")
|| result.code.contains("name: \"MyComponent\""),
"Should have component name. Got:\n{}",
result.code
);
}
#[test]
fn test_non_script_setup_typescript_preserved_when_is_ts() {
let source = r#"<script lang="ts">
interface Props {
name: string;
}
export default {
props: {} as Props
}
</script>
<template>
<div></div>
</template>"#;
let descriptor =
parse_sfc(source, SfcParseOptions::default()).expect("Failed to parse SFC");
let opts = SfcCompileOptions {
script: ScriptCompileOptions {
is_ts: true,
..Default::default()
},
template: TemplateCompileOptions {
is_ts: true,
..Default::default()
},
..Default::default()
};
let result = compile_sfc(&descriptor, opts).expect("Failed to compile SFC");
eprintln!(
"=== Non-script-setup TS preserved output ===\n{}",
result.code
);
assert!(
result.code.contains("interface Props") || result.code.contains("as Props"),
"Should preserve TypeScript when is_ts = true. Got:\n{}",
result.code
);
}
}