use crate::script::{transform_destructured_props, ScriptCompileContext};
use crate::types::SfcError;
use super::function_mode::{contains_top_level_await, dedupe_imports};
use super::macros::{
is_macro_call_line, is_multiline_macro_start, is_paren_macro_start, is_props_destructure_line,
};
use super::props::{
extract_emit_names_from_type, extract_prop_types_from_type, extract_with_defaults_defaults,
};
use super::typescript::transform_typescript_to_js;
use super::{ScriptCompileResult, TemplateParts};
pub fn compile_script_setup_inline(
content: &str,
component_name: &str,
is_ts: bool,
source_is_ts: bool,
template: TemplateParts<'_>,
normal_script_content: Option<&str>,
) -> Result<ScriptCompileResult, SfcError> {
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
let bump = vize_carton::Bump::new();
let mut output: vize_carton::Vec<u8> = vize_carton::Vec::with_capacity_in(4096, &bump);
let preserved_normal_script = normal_script_content
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let has_props_destructure = ctx.macros.props_destructure.is_some();
let needs_merge_defaults = has_props_destructure
&& ctx
.macros
.props_destructure
.as_ref()
.map(|d| d.bindings.values().any(|b| b.default.is_some()))
.unwrap_or(false);
let has_define_model = !ctx.macros.define_models.is_empty();
if needs_merge_defaults {
output.extend_from_slice(b"import { mergeDefaults as _mergeDefaults } from 'vue'\n");
}
if has_define_model {
output.extend_from_slice(b"import { useModel as _useModel } from 'vue'\n");
}
let needs_prop_type = is_ts
&& ctx
.macros
.define_props
.as_ref()
.is_some_and(|p| p.type_args.is_some());
if is_ts {
if needs_prop_type {
output.extend_from_slice(
b"import { defineComponent as _defineComponent, type PropType } from 'vue'\n",
);
} else {
output
.extend_from_slice(b"import { defineComponent as _defineComponent } from 'vue'\n");
}
}
if !template.imports.is_empty() {
output.extend_from_slice(template.imports.as_bytes());
output.push(b'\n');
}
let mut user_imports = Vec::new();
let mut setup_lines = Vec::new();
let mut ts_declarations: Vec<String> = Vec::new();
let mut in_import = false;
let mut import_buffer = String::new();
let mut in_destructure = false;
let mut destructure_buffer = String::new();
let mut brace_depth: i32 = 0;
let mut in_macro_call = false;
let mut macro_angle_depth: i32 = 0;
let mut in_paren_macro_call = false;
let mut paren_macro_depth: i32 = 0;
let mut waiting_for_macro_close = false;
let mut in_destructure_call = false;
let mut destructure_call_paren_depth: i32 = 0;
let mut in_object_literal = false;
let mut object_literal_buffer = String::new();
let mut object_literal_brace_depth: i32 = 0;
let mut in_ts_interface = false;
let mut ts_interface_brace_depth: i32 = 0;
let mut in_ts_type = false;
let mut ts_type_depth: i32 = 0; let mut in_template_literal = false;
for line in content.lines() {
let trimmed = line.trim();
if in_macro_call {
let line_no_arrow = trimmed.replace("=>", "");
macro_angle_depth += line_no_arrow.matches('<').count() as i32;
macro_angle_depth -= line_no_arrow.matches('>').count() as i32;
let trimmed_no_semi_m = trimmed.trim_end_matches(';');
if macro_angle_depth <= 0
&& (trimmed_no_semi_m.contains("()") || trimmed_no_semi_m.ends_with(')'))
{
in_macro_call = false;
}
continue;
}
if in_destructure_call {
destructure_call_paren_depth += trimmed.matches('(').count() as i32;
destructure_call_paren_depth -= trimmed.matches(')').count() as i32;
if destructure_call_paren_depth <= 0 {
in_destructure_call = false;
}
continue;
}
if in_paren_macro_call {
paren_macro_depth += trimmed.matches('(').count() as i32;
paren_macro_depth -= trimmed.matches(')').count() as i32;
if paren_macro_depth <= 0 {
in_paren_macro_call = false;
}
continue;
}
if waiting_for_macro_close {
destructure_buffer.push_str(line);
destructure_buffer.push('\n');
let line_no_arrow = trimmed.replace("=>", "");
macro_angle_depth += line_no_arrow.matches('<').count() as i32;
macro_angle_depth -= line_no_arrow.matches('>').count() as i32;
let trimmed_no_semi_w = trimmed.trim_end_matches(';');
if macro_angle_depth <= 0
&& (trimmed_no_semi_w.ends_with("()") || trimmed_no_semi_w.ends_with(')'))
{
waiting_for_macro_close = false;
destructure_buffer.clear();
}
continue;
}
if in_destructure {
destructure_buffer.push_str(line);
destructure_buffer.push('\n');
let line_no_arrow = trimmed.replace("=>", "");
brace_depth += trimmed.matches('{').count() as i32;
brace_depth -= trimmed.matches('}').count() as i32;
macro_angle_depth += line_no_arrow.matches('<').count() as i32;
macro_angle_depth -= line_no_arrow.matches('>').count() as i32;
if brace_depth <= 0 && macro_angle_depth <= 0 {
let is_props_macro = destructure_buffer.contains("defineProps")
|| destructure_buffer.contains("withDefaults");
let trimmed_no_semi = trimmed.trim_end_matches(';');
if is_props_macro
&& !trimmed_no_semi.ends_with("()")
&& !trimmed_no_semi.ends_with(')')
{
waiting_for_macro_close = true;
continue;
}
in_destructure = false;
if !is_props_macro {
for buf_line in destructure_buffer.lines() {
setup_lines.push(buf_line.to_string());
}
}
let paren_balance = destructure_buffer.matches('(').count() as i32
- destructure_buffer.matches(')').count() as i32;
if paren_balance > 0 {
in_destructure_call = true;
destructure_call_paren_depth = paren_balance;
}
destructure_buffer.clear();
}
continue;
}
if is_paren_macro_start(trimmed)
&& !trimmed.starts_with("const {")
&& !trimmed.starts_with("let {")
{
in_paren_macro_call = true;
paren_macro_depth =
trimmed.matches('(').count() as i32 - trimmed.matches(')').count() as i32;
continue;
}
if is_multiline_macro_start(trimmed)
&& !trimmed.starts_with("const {")
&& !trimmed.starts_with("let {")
{
in_macro_call = true;
macro_angle_depth =
trimmed.matches('<').count() as i32 - trimmed.matches('>').count() as i32;
continue;
}
if (trimmed.starts_with("const {")
|| trimmed.starts_with("let {")
|| trimmed.starts_with("var {"))
&& (trimmed.contains("defineProps<") || trimmed.contains("withDefaults("))
{
let trimmed_no_semi_d = trimmed.trim_end_matches(';');
if !trimmed_no_semi_d.ends_with("()") && !trimmed_no_semi_d.ends_with(')') {
in_destructure = true;
destructure_buffer = line.to_string() + "\n";
brace_depth =
trimmed.matches('{').count() as i32 - trimmed.matches('}').count() as i32;
macro_angle_depth =
trimmed.matches('<').count() as i32 - trimmed.matches('>').count() as i32;
continue;
} else {
continue;
}
}
if (trimmed.starts_with("const {")
|| trimmed.starts_with("let {")
|| trimmed.starts_with("var {"))
&& trimmed.contains('}')
&& trimmed.ends_with('=')
{
in_destructure = true;
destructure_buffer = line.to_string() + "\n";
brace_depth = 0; macro_angle_depth = 0;
continue;
}
if (trimmed.starts_with("const {")
|| trimmed.starts_with("let {")
|| trimmed.starts_with("var {"))
&& !trimmed.contains('}')
{
in_destructure = true;
destructure_buffer = line.to_string() + "\n";
brace_depth = trimmed.matches('{').count() as i32 - trimmed.matches('}').count() as i32;
macro_angle_depth = 0;
continue;
}
if is_props_destructure_line(trimmed) {
continue;
}
if in_object_literal {
object_literal_buffer.push_str(line);
object_literal_buffer.push('\n');
object_literal_brace_depth += trimmed.matches('{').count() as i32;
object_literal_brace_depth -= trimmed.matches('}').count() as i32;
if object_literal_brace_depth <= 0 {
for buf_line in object_literal_buffer.lines() {
setup_lines.push(buf_line.to_string());
}
in_object_literal = false;
object_literal_buffer.clear();
}
continue;
}
if (trimmed.starts_with("const ")
|| trimmed.starts_with("let ")
|| trimmed.starts_with("var "))
&& trimmed.contains('=')
&& trimmed.ends_with('{')
&& !trimmed.contains("defineProps")
&& !trimmed.contains("defineEmits")
&& !trimmed.contains("defineModel")
{
in_object_literal = true;
object_literal_buffer = line.to_string() + "\n";
object_literal_brace_depth =
trimmed.matches('{').count() as i32 - trimmed.matches('}').count() as i32;
continue;
}
let backtick_count = line
.chars()
.fold((0, false), |(count, escaped), c| {
if escaped {
(count, false)
} else if c == '\\' {
(count, true)
} else if c == '`' {
(count + 1, false)
} else {
(count, false)
}
})
.0;
let was_in_template_literal = in_template_literal;
if backtick_count % 2 == 1 {
in_template_literal = !in_template_literal;
}
if was_in_template_literal {
if !trimmed.is_empty() && !is_macro_call_line(trimmed) {
setup_lines.push(line.to_string());
}
continue;
}
if trimmed.starts_with("import ") {
if !trimmed.contains(" from ") && (trimmed.contains('\'') || trimmed.contains('"')) {
let mut imp = String::with_capacity(line.len() + 1);
imp.push_str(line);
imp.push('\n');
user_imports.push(imp);
continue;
}
in_import = true;
import_buffer.clear();
}
if in_import {
import_buffer.push_str(line);
import_buffer.push('\n');
if trimmed.ends_with(';') || (trimmed.contains(" from ") && !trimmed.ends_with(',')) {
user_imports.push(import_buffer.clone());
in_import = false;
}
continue;
}
if in_ts_interface {
if is_ts {
if let Some(last) = ts_declarations.last_mut() {
last.push('\n');
last.push_str(line);
}
}
ts_interface_brace_depth += trimmed.matches('{').count() as i32;
ts_interface_brace_depth -= trimmed.matches('}').count() as i32;
if ts_interface_brace_depth <= 0 {
in_ts_interface = false;
}
continue;
}
if trimmed.starts_with("interface ") || trimmed.starts_with("export interface ") {
in_ts_interface = true;
ts_interface_brace_depth =
trimmed.matches('{').count() as i32 - trimmed.matches('}').count() as i32;
if is_ts {
ts_declarations.push(line.to_string());
}
if ts_interface_brace_depth <= 0 {
in_ts_interface = false;
}
continue;
}
if trimmed.starts_with("declare ") {
let has_brace = trimmed.contains('{');
if has_brace {
let depth =
trimmed.matches('{').count() as i32 - trimmed.matches('}').count() as i32;
if depth > 0 {
in_ts_interface = true;
ts_interface_brace_depth = depth;
}
if is_ts {
ts_declarations.push(line.to_string());
}
} else {
if is_ts {
ts_declarations.push(line.to_string());
}
}
continue;
}
if in_ts_type {
if is_ts {
if let Some(last) = ts_declarations.last_mut() {
last.push('\n');
last.push_str(line);
}
}
let line_no_arrow = trimmed.replace("=>", "__");
ts_type_depth += trimmed.matches('{').count() as i32;
ts_type_depth -= trimmed.matches('}').count() as i32;
ts_type_depth += line_no_arrow.matches('<').count() as i32;
ts_type_depth -= line_no_arrow.matches('>').count() as i32;
ts_type_depth += trimmed.matches('(').count() as i32;
ts_type_depth -= trimmed.matches(')').count() as i32;
let is_union_continuation = trimmed.starts_with('|') || trimmed.starts_with('&');
if ts_type_depth <= 0
&& !is_union_continuation
&& (trimmed.ends_with(';')
|| (!trimmed.ends_with('|')
&& !trimmed.ends_with('&')
&& !trimmed.ends_with(',')
&& !trimmed.ends_with('{')))
{
in_ts_type = false;
}
continue;
}
if (trimmed.starts_with("type ")
&& trimmed[5..]
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '{'))
|| (trimmed.starts_with("export type ")
&& trimmed[12..]
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '{'))
{
let has_equals = trimmed.contains('=');
if has_equals {
let line_no_arrow = trimmed.replace("=>", "__");
ts_type_depth = trimmed.matches('{').count() as i32
- trimmed.matches('}').count() as i32
+ line_no_arrow.matches('<').count() as i32
- line_no_arrow.matches('>').count() as i32
+ trimmed.matches('(').count() as i32
- trimmed.matches(')').count() as i32;
if ts_type_depth <= 0
&& (trimmed.ends_with(';')
|| (!trimmed.ends_with('|')
&& !trimmed.ends_with('&')
&& !trimmed.ends_with(',')
&& !trimmed.ends_with('{')
&& !trimmed.ends_with('=')))
{
if is_ts {
ts_declarations.push(line.to_string());
}
continue;
}
if is_ts {
ts_declarations.push(line.to_string());
}
in_ts_type = true;
} else {
if is_ts {
ts_declarations.push(line.to_string());
}
}
continue;
}
if !trimmed.is_empty() && !is_macro_call_line(trimmed) {
setup_lines.push(line.to_string());
}
}
if !template.hoisted.is_empty() {
output.push(b'\n');
output.extend_from_slice(template.hoisted.as_bytes());
}
let deduped_imports = dedupe_imports(&user_imports);
for import in &deduped_imports {
output.extend_from_slice(import.as_bytes());
}
if !ts_declarations.is_empty() {
output.push(b'\n');
for decl in &ts_declarations {
output.extend_from_slice(decl.as_bytes());
output.push(b'\n');
}
}
let has_default_export = if let Some(ref normal_script) = preserved_normal_script {
output.push(b'\n');
output.extend_from_slice(normal_script.as_bytes());
output.push(b'\n');
normal_script.contains("const __default__")
} else {
false
};
let mut props_emits_buf: Vec<u8> = Vec::new();
let with_defaults_args = ctx
.macros
.with_defaults
.as_ref()
.map(|wd| extract_with_defaults_defaults(&wd.args));
let model_infos: Vec<(String, String, Option<String>)> = ctx
.macros
.define_models
.iter()
.map(|m| {
let model_name = if m.args.trim().is_empty() {
"modelValue".to_string()
} else {
let args = m.args.trim();
if args.starts_with('\'') || args.starts_with('"') {
args.trim_matches(|c| c == '\'' || c == '"')
.split(',')
.next()
.unwrap_or("modelValue")
.trim_matches(|c| c == '\'' || c == '"')
.to_string()
} else {
"modelValue".to_string()
}
};
let binding_name = m.binding_name.clone().unwrap_or_else(|| model_name.clone());
let options = if m.args.trim().is_empty() {
None
} else {
let args = m.args.trim();
if args.starts_with('{') {
Some(args.to_string())
} else if args.contains(',') {
args.split_once(',')
.map(|(_, opts)| opts.trim().to_string())
} else {
None
}
};
(model_name, binding_name, options)
})
.collect();
if let Some(ref props_macro) = ctx.macros.define_props {
if let Some(ref type_args) = props_macro.type_args {
let resolved_type_args =
resolve_type_args(type_args, &ctx.interfaces, &ctx.type_aliases);
let prop_types = extract_prop_types_from_type(&resolved_type_args);
if !prop_types.is_empty() || !model_infos.is_empty() {
props_emits_buf.extend_from_slice(b" props: {\n");
let total_items = prop_types.len() + model_infos.len();
let mut item_idx = 0;
for (name, prop_type) in &prop_types {
item_idx += 1;
props_emits_buf.extend_from_slice(b" ");
props_emits_buf.extend_from_slice(name.as_bytes());
props_emits_buf.extend_from_slice(b": { type: ");
props_emits_buf.extend_from_slice(prop_type.js_type.as_bytes());
if needs_prop_type {
if let Some(ref ts_type) = prop_type.ts_type {
if prop_type.js_type == "null" {
props_emits_buf.extend_from_slice(b" as unknown as PropType<");
} else {
props_emits_buf.extend_from_slice(b" as PropType<");
}
let normalized: String =
ts_type.split_whitespace().collect::<Vec<_>>().join(" ");
props_emits_buf.extend_from_slice(normalized.as_bytes());
props_emits_buf.push(b'>');
}
}
props_emits_buf.extend_from_slice(b", required: ");
props_emits_buf.extend_from_slice(if prop_type.optional {
b"false"
} else {
b"true"
});
let mut has_default = false;
if let Some(ref defaults) = with_defaults_args {
if let Some(default_val) = defaults.get(name.as_str()) {
props_emits_buf.extend_from_slice(b", default: ");
props_emits_buf.extend_from_slice(default_val.as_bytes());
has_default = true;
}
}
if !has_default {
if let Some(ref destructure) = ctx.macros.props_destructure {
if let Some(binding) = destructure.bindings.get(name.as_str()) {
if let Some(ref default_val) = binding.default {
props_emits_buf.extend_from_slice(b", default: ");
props_emits_buf.extend_from_slice(default_val.as_bytes());
}
}
}
}
props_emits_buf.extend_from_slice(b" }");
if item_idx < total_items {
props_emits_buf.push(b',');
}
props_emits_buf.push(b'\n');
}
for (model_name, _, options) in &model_infos {
props_emits_buf.extend_from_slice(b" \"");
props_emits_buf.extend_from_slice(model_name.as_bytes());
props_emits_buf.extend_from_slice(b"\": ");
if let Some(opts) = options {
props_emits_buf.extend_from_slice(opts.as_bytes());
} else {
props_emits_buf.extend_from_slice(b"{}");
}
props_emits_buf.extend_from_slice(b",\n");
}
if props_emits_buf.ends_with(b",\n") {
let len = props_emits_buf.len();
props_emits_buf[len - 2] = b'\n';
props_emits_buf.truncate(len - 1);
}
props_emits_buf.extend_from_slice(b" },\n");
}
} else if !props_macro.args.is_empty() {
if needs_merge_defaults {
let destructure = ctx.macros.props_destructure.as_ref().unwrap();
props_emits_buf.extend_from_slice(b" props: /*@__PURE__*/_mergeDefaults(");
props_emits_buf.extend_from_slice(props_macro.args.as_bytes());
props_emits_buf.extend_from_slice(b", {\n");
let defaults: Vec<_> = destructure
.bindings
.iter()
.filter_map(|(k, b)| b.default.as_ref().map(|d| (k.as_str(), d.as_str())))
.collect();
for (i, (key, default_val)) in defaults.iter().enumerate() {
props_emits_buf.extend_from_slice(b" ");
props_emits_buf.extend_from_slice(key.as_bytes());
props_emits_buf.extend_from_slice(b": ");
props_emits_buf.extend_from_slice(default_val.as_bytes());
if i < defaults.len() - 1 {
props_emits_buf.push(b',');
}
props_emits_buf.push(b'\n');
}
props_emits_buf.extend_from_slice(b"}),\n");
} else {
props_emits_buf.extend_from_slice(b" props: ");
props_emits_buf.extend_from_slice(props_macro.args.as_bytes());
props_emits_buf.extend_from_slice(b",\n");
}
}
}
if !model_infos.is_empty() && ctx.macros.define_props.is_none() {
props_emits_buf.extend_from_slice(b" props: {\n");
for (model_name, _binding_name, options) in &model_infos {
props_emits_buf.extend_from_slice(b" \"");
props_emits_buf.extend_from_slice(model_name.as_bytes());
props_emits_buf.extend_from_slice(b"\": ");
if let Some(opts) = options {
props_emits_buf.extend_from_slice(opts.as_bytes());
} else {
props_emits_buf.extend_from_slice(b"{}");
}
props_emits_buf.extend_from_slice(b",\n");
props_emits_buf.extend_from_slice(b" \"");
if model_name == "modelValue" {
props_emits_buf.extend_from_slice(b"modelModifiers");
} else {
props_emits_buf.extend_from_slice(model_name.as_bytes());
props_emits_buf.extend_from_slice(b"Modifiers");
}
props_emits_buf.extend_from_slice(b"\": {},\n");
}
if props_emits_buf.ends_with(b",\n") {
let len = props_emits_buf.len();
props_emits_buf[len - 2] = b'\n';
props_emits_buf.truncate(len - 1);
}
props_emits_buf.extend_from_slice(b" },\n");
}
let mut all_emits: Vec<String> = Vec::new();
if let Some(ref emits_macro) = ctx.macros.define_emits {
if !emits_macro.args.is_empty() {
let args = emits_macro.args.trim();
if args.starts_with('[') && args.ends_with(']') {
let inner = &args[1..args.len() - 1];
for part in inner.split(',') {
let name = part.trim().trim_matches(|c| c == '\'' || c == '"');
if !name.is_empty() {
all_emits.push(name.to_string());
}
}
}
} else if let Some(ref type_args) = emits_macro.type_args {
let emit_names = extract_emit_names_from_type(type_args);
all_emits.extend(emit_names);
}
}
for (model_name, _, _) in &model_infos {
let mut name = String::with_capacity(7 + model_name.len());
name.push_str("update:");
name.push_str(model_name);
all_emits.push(name);
}
if !all_emits.is_empty() {
props_emits_buf.extend_from_slice(b" emits: [");
for (i, name) in all_emits.iter().enumerate() {
if i > 0 {
props_emits_buf.extend_from_slice(b", ");
}
props_emits_buf.push(b'"');
props_emits_buf.extend_from_slice(name.as_bytes());
props_emits_buf.push(b'"');
}
props_emits_buf.extend_from_slice(b"],\n");
}
let setup_code = setup_lines.join("\n");
let transformed_setup = if let Some(ref destructure) = ctx.macros.props_destructure {
transform_destructured_props(&setup_code, destructure)
} else {
setup_code
};
let mut hoisted_lines: Vec<String> = Vec::new();
let mut setup_body_lines: Vec<String> = Vec::new();
let mut in_multiline_value = false;
for line in transformed_setup.lines() {
let trimmed = line.trim();
if in_multiline_value {
setup_body_lines.push(line.to_string());
let backticks = trimmed
.chars()
.fold((0usize, false), |(count, escaped), c| {
if escaped {
(count, false)
} else if c == '\\' {
(count, true)
} else if c == '`' {
(count + 1, false)
} else {
(count, false)
}
})
.0;
if backticks % 2 == 1 {
in_multiline_value = false;
}
continue;
}
if trimmed.starts_with("const ") && !trimmed.starts_with("const {") {
if let Some(eq_pos) = trimmed.find('=') {
let value_part = trimmed[eq_pos + 1..].trim();
let backticks = value_part
.chars()
.fold((0usize, false), |(count, escaped), c| {
if escaped {
(count, false)
} else if c == '\\' {
(count, true)
} else if c == '`' {
(count + 1, false)
} else {
(count, false)
}
})
.0;
if backticks % 2 == 1 {
in_multiline_value = true;
setup_body_lines.push(line.to_string());
continue;
}
}
if let Some(name) = extract_const_name(trimmed) {
if matches!(
ctx.bindings.bindings.get(&name),
Some(crate::types::BindingType::LiteralConst)
) {
hoisted_lines.push(line.to_string());
continue;
}
}
}
setup_body_lines.push(line.to_string());
}
if !hoisted_lines.is_empty() {
for line in &hoisted_lines {
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
}
}
output.push(b'\n');
let has_options = ctx.macros.define_options.is_some();
let has_emit = ctx.macros.define_emits.is_some();
let has_emit_binding = ctx
.macros
.define_emits
.as_ref()
.map(|e| e.binding_name.is_some())
.unwrap_or(false);
let has_expose = ctx.macros.define_expose.is_some();
if has_options {
output.extend_from_slice(b"export default /*@__PURE__*/Object.assign(");
let options_args = ctx.macros.define_options.as_ref().unwrap().args.trim();
output.extend_from_slice(options_args.as_bytes());
output.extend_from_slice(b", {\n");
} else if has_default_export {
output.extend_from_slice(b"export default /*@__PURE__*/Object.assign(__default__, {\n");
} else if is_ts {
output.extend_from_slice(b"export default /*@__PURE__*/_defineComponent({\n");
} else {
output.extend_from_slice(b"export default {\n");
}
output.extend_from_slice(b" __name: '");
output.extend_from_slice(component_name.as_bytes());
output.extend_from_slice(b"',\n");
output.extend_from_slice(&props_emits_buf);
let mut setup_args = Vec::new();
if has_expose {
setup_args.push("expose: __expose");
}
if has_emit {
if has_emit_binding {
setup_args.push("emit: __emit");
} else {
setup_args.push("emit: $emit");
}
}
let has_typed_props = is_ts
&& ctx
.macros
.define_props
.as_ref()
.is_some_and(|p| p.type_args.is_some() || !p.args.is_empty());
let props_param = if has_typed_props && !needs_prop_type {
"__props: any"
} else {
"__props"
};
let setup_code_for_await_check: String = setup_lines.join("\n");
let is_async = contains_top_level_await(&setup_code_for_await_check, source_is_ts);
let async_prefix = if is_async {
" async setup("
} else {
" setup("
};
if setup_args.is_empty() {
output.extend_from_slice(async_prefix.as_bytes());
output.extend_from_slice(props_param.as_bytes());
output.extend_from_slice(b") {\n");
} else {
output.extend_from_slice(async_prefix.as_bytes());
output.extend_from_slice(props_param.as_bytes());
output.extend_from_slice(b", { ");
output.extend_from_slice(setup_args.join(", ").as_bytes());
output.extend_from_slice(b" }) {\n");
}
output.push(b'\n');
if let Some(ref emits_macro) = ctx.macros.define_emits {
if let Some(ref binding_name) = emits_macro.binding_name {
output.extend_from_slice(b"const ");
output.extend_from_slice(binding_name.as_bytes());
output.extend_from_slice(b" = __emit\n");
}
}
if let Some(ref props_macro) = ctx.macros.define_props {
if let Some(ref binding_name) = props_macro.binding_name {
output.extend_from_slice(b"const ");
output.extend_from_slice(binding_name.as_bytes());
output.extend_from_slice(b" = __props\n");
}
}
if !model_infos.is_empty() {
for (model_name, binding_name, _) in &model_infos {
output.extend_from_slice(b"const ");
output.extend_from_slice(binding_name.as_bytes());
output.extend_from_slice(b" = _useModel(__props, \"");
output.extend_from_slice(model_name.as_bytes());
output.extend_from_slice(b"\")\n");
}
}
for line in &setup_body_lines {
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
}
if let Some(ref expose_macro) = ctx.macros.define_expose {
let args = expose_macro.args.trim();
output.extend_from_slice(b"__expose(");
output.extend_from_slice(args.as_bytes());
output.extend_from_slice(b")\n");
}
output.push(b'\n');
if !template.render_body.is_empty() {
if is_ts {
output.extend_from_slice(b"return (_ctx: any,_cache: any) => {\n");
} else {
output.extend_from_slice(b"return (_ctx, _cache) => {\n");
}
for line in template.preamble.lines() {
if !line.trim().is_empty() {
output.extend_from_slice(b" ");
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
}
}
if !template.preamble.is_empty() {
output.push(b'\n');
}
let mut first_line = true;
for line in template.render_body.lines() {
if first_line {
output.extend_from_slice(b" return ");
output.extend_from_slice(line.as_bytes());
first_line = false;
} else {
output.push(b'\n');
if !line.trim().is_empty() {
output.extend_from_slice(b" ");
}
output.extend_from_slice(line.as_bytes());
}
}
output.push(b'\n');
output.extend_from_slice(b"}\n");
} else {
use crate::types::BindingType;
let setup_bindings: Vec<&String> = ctx
.bindings
.bindings
.iter()
.filter(|(_, bt)| {
matches!(
bt,
BindingType::SetupLet
| BindingType::SetupMaybeRef
| BindingType::SetupRef
| BindingType::SetupReactiveConst
| BindingType::SetupConst
| BindingType::LiteralConst
)
})
.map(|(name, _)| name)
.collect();
if !setup_bindings.is_empty() {
output.extend_from_slice(b"return { ");
for (i, name) in setup_bindings.iter().enumerate() {
if i > 0 {
output.extend_from_slice(b", ");
}
output.extend_from_slice(name.as_bytes());
}
output.extend_from_slice(b" }\n");
}
}
output.extend_from_slice(b"}\n");
output.push(b'\n');
if has_options || has_default_export || is_ts {
output.extend_from_slice(b"})\n");
} else {
output.extend_from_slice(b"}\n");
}
let output_str = unsafe { String::from_utf8_unchecked(output.into_iter().collect()) };
let final_code = if is_ts || !source_is_ts {
let mut code = output_str;
if is_ts {
code = code.replace("$event => (", "($event: any) => (");
}
code
} else {
transform_typescript_to_js(&output_str)
};
Ok(ScriptCompileResult {
code: final_code,
bindings: Some(ctx.bindings),
})
}
fn extract_const_name(line: &str) -> Option<String> {
let rest = line.trim().strip_prefix("const ")?;
if rest.starts_with('{') || rest.starts_with('[') {
return None;
}
let name_end = rest.find(|c: char| c == '=' || c == ':' || c.is_whitespace())?;
let name = rest[..name_end].trim();
if name.is_empty() {
return None;
}
Some(name.to_string())
}
fn resolve_type_args(
type_args: &str,
interfaces: &vize_carton::FxHashMap<String, String>,
type_aliases: &vize_carton::FxHashMap<String, String>,
) -> String {
let content = type_args.trim();
if content.starts_with('{') {
return content.to_string();
}
if content.contains('&') {
let parts: Vec<&str> = content.split('&').collect();
let mut merged_props = Vec::new();
for part in parts {
let resolved = resolve_single_type_ref(part.trim(), interfaces, type_aliases);
if let Some(body) = resolved {
let body = body.trim();
let inner = if body.starts_with('{') && body.ends_with('}') {
&body[1..body.len() - 1]
} else {
body
};
let trimmed = inner.trim();
if !trimmed.is_empty() {
merged_props.push(trimmed.to_string());
}
}
}
if !merged_props.is_empty() {
let joined = merged_props.join("; ");
let mut result = String::with_capacity(joined.len() + 4);
result.push_str("{ ");
result.push_str(&joined);
result.push_str(" }");
return result;
}
return content.to_string();
}
if let Some(body) = resolve_single_type_ref(content, interfaces, type_aliases) {
let body = body.trim();
if body.starts_with('{') {
return body.to_string();
}
let mut result = String::with_capacity(body.len() + 4);
result.push_str("{ ");
result.push_str(body);
result.push_str(" }");
return result;
}
content.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn compile_setup(script_content: &str) -> String {
let empty_template = TemplateParts {
imports: "",
hoisted: "",
preamble: "",
render_body: "null",
};
let result = compile_script_setup_inline(
script_content,
"TestComponent",
false, true, empty_template,
None,
)
.expect("compilation should succeed");
result.code
}
fn compile_setup_ts(script_content: &str) -> String {
let empty_template = TemplateParts {
imports: "",
hoisted: "",
preamble: "",
render_body: "null",
};
let result = compile_script_setup_inline(
script_content,
"TestComponent",
true, true, empty_template,
None,
)
.expect("compilation should succeed");
result.code
}
#[test]
fn test_declare_global_not_in_setup_body_ts() {
let content = r#"
import { ref } from 'vue'
const handleClick = () => {
console.log('click')
}
declare global {
interface Window {
EyeDropper: any
}
}
const x = ref(0)
"#;
let output = compile_setup_ts(content);
let setup_start = output.find("setup(").expect("should have setup function");
let setup_body = &output[setup_start..];
assert!(
!setup_body.contains("declare global"),
"declare global should NOT be inside setup function body. Got:\n{}",
output
);
}
#[test]
fn test_export_type_reexport_stripped() {
let content = r#"
import { ref } from 'vue'
import type { FilterType } from './types'
export type { FilterType }
const x = ref(0)
"#;
let output = compile_setup(content);
let setup_start = output.find("setup(").expect("should have setup");
let setup_body = &output[setup_start..];
assert!(
!setup_body.contains("export type"),
"export type re-export should not be inside setup body. Got:\n{}",
output
);
}
#[test]
fn test_type_as_variable_at_line_start() {
let content = r#"
import { ref } from 'vue'
const type = ref('material-symbols')
const identifier =
type === 'material-symbols' ? 'name' : 'ligature'
"#;
let output = compile_setup(content);
assert!(
output.contains("type ==="),
"`type ===` continuation line should be preserved. Got:\n{}",
output
);
}
#[test]
fn test_destructure_with_multiline_function_call() {
let content = r#"
import { ref, toRef } from 'vue'
import { useSomething } from './useSomething'
const fileInputRef = ref()
const {
handleSelect,
handleChange,
} = useSomething(
fileInputRef,
{
onError: (e) => console.log(e),
onSuccess: () => console.log('ok'),
},
toRef(() => 'test'),
)
const other = ref(1)
"#;
let output = compile_setup(content);
assert!(
!output.contains("fileInputRef,"),
"Function call args should not leak as bare statements. Got:\n{}",
output
);
assert!(
output.contains("const other = ref(1)"),
"Code after destructure should be present. Got:\n{}",
output
);
}
#[test]
fn test_side_effect_import_without_semicolons() {
let content = r#"
import { watch } from 'vue'
import '@/css/oldReset.scss'
const { dialogRef } = provideDialog()
watch(
dialogRef,
(val) => {
console.log(val)
},
{ immediate: true },
)
"#;
let output = compile_setup_ts(content);
assert!(
output.contains("watch("),
"watch() call should be in setup body. Got:\n{}",
output
);
}
#[test]
fn test_export_type_with_arrow_function_member() {
let content = r#"
import { computed } from 'vue'
import { useRoute } from 'vue-router'
export type MenuSelectorOption = {
label: string
onClick: () => void
}
const route = useRoute()
const heading = computed(() => route.name)
"#;
let output = compile_setup_ts(content);
assert!(
output.contains("export type MenuSelectorOption"),
"export type should be at module level. Got:\n{}",
output
);
let setup_start = output.find("setup(").expect("should have setup");
let setup_body = &output[setup_start..];
assert!(
setup_body.contains("const route = useRoute()"),
"const route should be inside setup body. Got:\n{}",
output
);
}
#[test]
fn test_define_props_with_trailing_semicolon() {
let content = r#"
import { ref } from 'vue'
interface Props {
msg: string
}
const { msg } = defineProps<Props>();
const count = ref(0)
"#;
let output = compile_setup(content);
assert!(
output.contains("props: {"),
"should generate props declaration even with trailing semicolon. Got:\n{}",
output
);
assert!(
output.contains("msg:"),
"should include msg prop. Got:\n{}",
output
);
assert!(
output.contains("const count = ref(0)"),
"code after defineProps should be present. Got:\n{}",
output
);
}
#[test]
fn test_multiline_define_props_with_trailing_semicolon() {
let content = r#"
import { ref } from 'vue'
const { label, disabled } = defineProps<{
label: string
disabled?: boolean
}>();
const x = ref(1)
"#;
let output = compile_setup(content);
assert!(
output.contains("props: {"),
"should generate props declaration for multiline defineProps with semicolon. Got:\n{}",
output
);
assert!(
output.contains("const x = ref(1)"),
"code after defineProps should be present. Got:\n{}",
output
);
}
#[test]
fn test_with_defaults_trailing_semicolon() {
let content = r#"
import { ref } from 'vue'
interface Props {
msg: string
count?: number
}
const { msg, count } = withDefaults(defineProps<Props>(), {
count: 0,
});
const x = ref(1)
"#;
let output = compile_setup(content);
assert!(
output.contains("props: {"),
"should generate props declaration for withDefaults with semicolon. Got:\n{}",
output
);
assert!(
output.contains("const x = ref(1)"),
"code after withDefaults should be present. Got:\n{}",
output
);
}
fn compile_setup_no_template(script_content: &str) -> String {
let empty_template = TemplateParts {
imports: "",
hoisted: "",
preamble: "",
render_body: "",
};
let result = compile_script_setup_inline(
script_content,
"TestComponent",
false,
true,
empty_template,
None,
)
.expect("compilation should succeed");
result.code
}
#[test]
fn test_no_template_returns_setup_bindings() {
let content = r#"
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
"#;
let output = compile_setup_no_template(content);
assert!(
output.contains("return {"),
"no-template case should return setup bindings. Got:\n{}",
output
);
assert!(
output.contains("count"),
"should return count binding. Got:\n{}",
output
);
assert!(
output.contains("doubled"),
"should return doubled binding. Got:\n{}",
output
);
}
#[test]
fn test_no_template_returns_imported_bindings() {
let content = r#"
import { onMounted } from 'vue'
onMounted(() => {
console.log('mounted')
})
"#;
let output = compile_setup_no_template(content);
assert!(
output.contains("return {") && output.contains("onMounted"),
"no-template case should return imported bindings too (for runtime template access). Got:\n{}",
output
);
}
#[test]
fn test_export_type_generates_props_declaration() {
let content = r#"
export type MenuItemProps = {
id: string
label: string
routeName: string
disabled?: boolean
}
const { label, disabled, routeName } = defineProps<MenuItemProps>()
"#;
let output = compile_setup(content);
assert!(
output.contains("props: {"),
"should generate props declaration for export type. Got:\n{}",
output
);
assert!(
output.contains("label:") && output.contains("String"),
"should include label prop. Got:\n{}",
output
);
assert!(
output.contains("routeName:") && output.contains("String"),
"should include routeName prop. Got:\n{}",
output
);
assert!(
output.contains("disabled:"),
"should include disabled prop. Got:\n{}",
output
);
}
#[test]
fn test_define_props_destructure_value_on_next_line() {
let content = r#"
import { computed } from 'vue'
interface TimetableCell {
type: string
title: string
startTime: string
}
const { type, title, startTime } =
defineProps<TimetableCell>();
const accentColor = computed(() => type === 'event' ? 'primary' : 'secondary')
"#;
let output = compile_setup(content);
assert!(
output.contains("props: {"),
"should generate props declaration for next-line defineProps. Got:\n{}",
output
);
assert!(
output.contains("type:") && output.contains("String"),
"should include type prop. Got:\n{}",
output
);
assert!(
output.contains("title:") && output.contains("String"),
"should include title prop. Got:\n{}",
output
);
let setup_start = output.find("setup(").expect("should have setup");
let setup_body = &output[setup_start..];
assert!(
setup_body.contains("__props.type"),
"destructured prop 'type' should be rewritten to __props.type in setup body. Got:\n{}",
output
);
assert!(
!setup_body.contains("const { __props."),
"destructure declaration should NOT appear in setup body. Got:\n{}",
output
);
}
#[test]
fn test_define_props_destructure_value_on_next_line_with_semicolon() {
let content = r#"
import { ref } from 'vue'
interface Props {
msg: string
count: number
}
const { msg, count } =
defineProps<Props>();
const doubled = ref(count * 2)
"#;
let output = compile_setup(content);
assert!(
output.contains("props: {"),
"should generate props declaration. Got:\n{}",
output
);
assert!(
output.contains("msg:") && output.contains("String"),
"should include msg prop. Got:\n{}",
output
);
}
#[test]
fn test_non_props_destructure_value_on_next_line() {
let content = r#"
import { ref, toRefs } from 'vue'
const state = ref({ x: 1, y: 2 })
const { x, y } =
toRefs(state.value)
const sum = ref(x.value + y.value)
"#;
let output = compile_setup(content);
assert!(
output.contains("toRefs("),
"non-props destructure should be preserved in setup body. Got:\n{}",
output
);
assert!(
output.contains("const sum = ref("),
"code after destructure should be present. Got:\n{}",
output
);
}
}
fn resolve_single_type_ref(
name: &str,
interfaces: &vize_carton::FxHashMap<String, String>,
type_aliases: &vize_carton::FxHashMap<String, String>,
) -> Option<String> {
let base_name = if let Some(idx) = name.find('<') {
name[..idx].trim()
} else {
name.trim()
};
if let Some(body) = interfaces.get(base_name) {
return Some(body.clone());
}
if let Some(body) = type_aliases.get(base_name) {
return Some(body.clone());
}
None
}