mod bindings;
mod helpers;
mod normal_script;
mod styles;
#[cfg(test)]
mod tests;
use crate::compile_script::artifacts::{erase_artifact_macro_statements, extract_macro_artifacts};
use crate::compile_script::lazy_hydration::transform_lazy_hydration_macros;
use crate::compile_script::{compile_script_setup_inline_with_context, TemplateParts};
use crate::compile_template::{
compile_template_block, compile_template_block_vapor, extract_template_parts,
extract_template_parts_full, TemplateBlockCompileContext,
};
use crate::rewrite_default::rewrite_default;
use crate::script::ScriptCompileContext;
use crate::types::{
BindingType, SfcCompileOptions, SfcCompileResult, SfcDescriptor, SfcError, SfcMacroArtifact,
};
use self::bindings::{croquis_to_legacy_bindings, register_normal_script_bindings};
use self::helpers::{
demote_v_model_reactive_const_bindings, extract_component_name, generate_scope_id,
};
use self::normal_script::extract_normal_script_content;
use self::styles::compile_styles;
pub use crate::compile_script::ScriptCompileResult;
use vize_carton::{profile, String, ToCompactString};
fn create_vapor_ssr_fallback_warning(descriptor: &SfcDescriptor) -> SfcError {
SfcError {
message: "SFC Vapor SSR is not supported yet; falling back to standard SSR output."
.to_compact_string(),
code: Some("VAPOR_SSR_FALLBACK".to_compact_string()),
loc: descriptor
.template
.as_ref()
.map(|template| template.loc.clone()),
}
}
fn create_v_model_reactive_const_warning(
script_setup: &crate::types::SfcScriptBlock<'_>,
binding_name: &str,
) -> SfcError {
let mut message = String::from("`v-model` cannot update the const reactive binding `");
message.push_str(binding_name);
message.push_str("`. The compiler transformed it to `let` so the update can work.");
SfcError {
message,
code: Some("V_MODEL_CONST_REACTIVE_DEMOTED".to_compact_string()),
loc: Some(script_setup.loc.clone()),
}
}
fn is_ts_lang(lang: Option<&str>) -> bool {
matches!(lang, Some("ts" | "tsx"))
}
fn extract_descriptor_macro_artifacts(descriptor: &SfcDescriptor) -> Vec<SfcMacroArtifact> {
let mut artifacts = Vec::new();
if let Some(script) = descriptor.script.as_ref() {
artifacts.extend(extract_macro_artifacts(&script.content, script.loc.start));
}
if let Some(script_setup) = descriptor.script_setup.as_ref() {
artifacts.extend(extract_macro_artifacts(
&script_setup.content,
script_setup.loc.start,
));
}
artifacts.sort_by_key(|artifact| artifact.start);
artifacts
}
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::default();
let mut css = None;
let macro_artifacts = extract_descriptor_macro_artifacts(descriptor);
let filename = options.script.id.as_deref().unwrap_or("anonymous.vue");
let has_styles = !descriptor.styles.is_empty();
let has_scoped = descriptor.styles.iter().any(|s| s.scoped);
let needs_scope_id =
has_styles || !descriptor.css_vars.is_empty() || options.scope_id.is_some();
let scope_id = if needs_scope_id {
options
.scope_id
.clone()
.unwrap_or_else(|| generate_scope_id(filename))
} else {
String::default()
};
let vapor_requested = options.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);
if descriptor.template.is_some() && options.template.ssr && vapor_requested {
warnings.push(create_vapor_ssr_fallback_warning(descriptor));
}
let is_vapor = !options.template.ssr && vapor_requested;
let is_ts = options.script.is_ts || options.template.is_ts;
let template_is_ts = options.template.is_ts
|| descriptor
.script_setup
.as_ref()
.is_some_and(|s| is_ts_lang(s.lang.as_deref()))
|| descriptor
.script
.as_ref()
.is_some_and(|s| is_ts_lang(s.lang.as_deref()));
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 template_result = if is_vapor {
profile!(
"atelier.sfc.template.vapor",
compile_template_block_vapor(
template,
&scope_id,
has_scoped,
None,
options.template.custom_renderer,
)
)
} else {
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);
profile!(
"atelier.sfc.template.compile",
compile_template_block(
template,
&template_opts,
TemplateBlockCompileContext {
scope_id: &scope_id,
apply_scope_id: options.template.ssr && has_scoped,
is_ts: template_is_ts,
component_name: Some(&component_name),
bindings: None,
croquis: None,
},
)
)
};
match template_result {
Ok(template_code) => {
code = template_code;
if is_vapor {
code.push_str("const _sfc_main = { __vapor: true }\n");
code.push_str("_sfc_main.render = render\n");
code.push_str("export default _sfc_main\n");
} else if options.template.ssr {
code.push_str("const _sfc_main = {}\n");
code.push_str("_sfc_main.ssrRender = ssrRender\n");
code.push_str("export default _sfc_main\n");
}
}
Err(e) => errors.push(e),
}
let all_css = profile!(
"atelier.sfc.styles",
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,
macro_artifacts,
});
}
if has_script && !has_script_setup {
let script = descriptor.script.as_ref().unwrap();
let lazy_hydration_transform = transform_lazy_hydration_macros(&script.content);
let script_source = lazy_hydration_transform
.as_ref()
.map(|result| result.code.as_str())
.unwrap_or(&script.content);
let script_content = erase_artifact_macro_statements(script_source)
.unwrap_or_else(|| script_source.to_compact_string());
let source_is_ts = script
.lang
.as_ref()
.is_some_and(|l| l == "ts" || l == "tsx");
let (rewritten_script, _has_default) = profile!(
"atelier.sfc.normal_script.rewrite_default",
rewrite_default(&script_content, "_sfc_main", source_is_ts)
);
let mut final_script = if source_is_ts && !is_ts {
profile!(
"atelier.sfc.normal_script.ts_to_js",
crate::compile_script::typescript::transform_typescript_to_js(&rewritten_script)
)
} else {
rewritten_script
};
if let Some(transform) = lazy_hydration_transform {
let mut script_with_preamble = transform.preamble;
script_with_preamble.push_str(&final_script);
final_script = script_with_preamble;
}
if has_template {
let template = descriptor.template.as_ref().unwrap();
let template_result = if is_vapor {
profile!(
"atelier.sfc.template.vapor",
compile_template_block_vapor(
template,
&scope_id,
has_scoped,
None,
options.template.custom_renderer,
)
)
} else {
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);
profile!(
"atelier.sfc.template.compile",
compile_template_block(
template,
&template_opts,
TemplateBlockCompileContext {
scope_id: &scope_id,
apply_scope_id: options.template.ssr && has_scoped,
is_ts: template_is_ts,
component_name: Some(&component_name),
bindings: None,
croquis: None,
},
)
)
};
match template_result {
Ok(template_code) => {
code.push_str(&template_code);
code.push_str(&final_script);
code.push('\n');
if is_vapor {
code.push_str("_sfc_main.__vapor = true\n");
}
if options.template.ssr {
code.push_str("_sfc_main.ssrRender = ssrRender\n");
} else {
code.push_str("_sfc_main.render = render\n");
}
code.push_str("export default _sfc_main\n");
}
Err(e) => {
errors.push(e);
code = final_script.clone();
code.push('\n');
}
}
} else {
code.push_str(&final_script);
if is_vapor {
code.push_str("\n_sfc_main.__vapor = true");
}
code.push_str("\nexport default _sfc_main\n");
}
let all_css = profile!(
"atelier.sfc.styles",
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,
macro_artifacts,
});
}
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_compact_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(profile!(
"atelier.sfc.normal_script.extract",
extract_normal_script_content(&script.content, source_is_ts, is_ts)
))
} else {
None
};
let lazy_hydration_transform = transform_lazy_hydration_macros(&script_setup.content);
let mut script_setup_content = lazy_hydration_transform
.as_ref()
.map(|result| result.code.clone())
.unwrap_or_else(|| script_setup.content.to_compact_string());
let mut croquis = profile!(
"atelier.sfc.script_setup.croquis",
crate::script::analyze_script_setup_to_summary(&script_setup_content)
);
let mut script_bindings = croquis_to_legacy_bindings(&croquis.bindings);
let mut ctx = profile!(
"atelier.sfc.script_context.new",
ScriptCompileContext::new(&script_setup_content)
);
if has_script {
let script = descriptor.script.as_ref().unwrap();
profile!(
"atelier.sfc.script_context.collect_normal_types",
ctx.collect_types_from(&script.content)
);
}
profile!(
"atelier.sfc.script_context.collect_setup_import_types",
ctx.collect_imported_types_from_path(&script_setup_content, filename)
);
if has_script {
let script = descriptor.script.as_ref().unwrap();
profile!(
"atelier.sfc.script_context.collect_normal_import_types",
ctx.collect_imported_types_from_path(&script.content, filename)
);
}
profile!("atelier.sfc.script_context.analyze", 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 let Some(ref emits_macro) = ctx.macros.define_emits {
if let Some(ref binding_name) = emits_macro.binding_name {
script_bindings
.bindings
.entry(binding_name.clone())
.or_insert(BindingType::SetupConst);
} else {
script_bindings
.bindings
.entry("$emit".to_compact_string())
.or_insert(BindingType::SetupConst);
}
}
if has_script {
let script = descriptor.script.as_ref().unwrap();
profile!(
"atelier.sfc.normal_script.register_bindings",
register_normal_script_bindings(&script.content, &mut script_bindings)
);
}
if let Some(template) = &descriptor.template {
let demoted_ids = profile!(
"atelier.sfc.script_setup.demote_v_model_reactive_consts",
demote_v_model_reactive_const_bindings(
&template.content,
script_setup.lang.as_deref(),
&mut script_setup_content,
&mut ctx,
&mut script_bindings,
&mut croquis,
)
);
for binding_name in demoted_ids {
warnings.push(create_v_model_reactive_const_warning(
script_setup,
&binding_name,
));
}
}
let template_result = if let Some(template) = &descriptor.template {
if is_vapor {
Some(profile!(
"atelier.sfc.template.vapor",
compile_template_block_vapor(
template,
&scope_id,
has_scoped,
Some(&script_bindings),
options.template.custom_renderer,
)
))
} else {
Some(profile!(
"atelier.sfc.template.compile",
compile_template_block(
template,
&options.template,
TemplateBlockCompileContext {
scope_id: &scope_id,
apply_scope_id: options.template.ssr && has_scoped,
is_ts: template_is_ts,
component_name: Some(&component_name),
bindings: Some(&script_bindings),
croquis: Some(croquis),
},
)
))
}
} else {
None
};
let (
template_imports,
template_hoisted,
template_render_fn,
template_render_fn_name,
template_preamble,
render_body,
) = match &template_result {
Some(Ok(template_code)) => {
if is_vapor || options.template.ssr {
let (imports, hoisted, render_fn, render_fn_name) = profile!(
"atelier.sfc.template.extract_parts_full",
extract_template_parts_full(template_code)
);
(
imports,
hoisted,
render_fn,
render_fn_name,
String::default(),
String::default(),
)
} else {
let (imports, hoisted, preamble, body, render_fn_name) = profile!(
"atelier.sfc.template.extract_parts",
extract_template_parts(template_code)
);
(
imports,
hoisted,
String::default(),
render_fn_name,
preamble,
body,
)
}
}
Some(Err(e)) => {
errors.push(e.clone());
(
String::default(),
String::default(),
String::default(),
"",
String::default(),
String::default(),
)
}
None => (
String::default(),
String::default(),
String::default(),
"",
String::default(),
String::default(),
),
};
let source_is_ts = script_setup
.lang
.as_ref()
.is_some_and(|l| l == "ts" || l == "tsx");
let script_result = profile!(
"atelier.sfc.script_setup.inline_compile",
compile_script_setup_inline_with_context(
ctx,
&script_setup_content,
&component_name,
is_ts,
source_is_ts,
is_vapor,
TemplateParts {
imports: &template_imports,
hoisted: &template_hoisted,
render_fn: &template_render_fn,
render_fn_name: template_render_fn_name,
preamble: &template_preamble,
render_body: &render_body,
render_is_block: is_vapor,
},
normal_script_content.as_deref(),
&descriptor.css_vars,
&scope_id,
)
)?;
if let Some(transform) = lazy_hydration_transform {
code.push_str(&transform.preamble);
}
code.push_str(&script_result.code);
let all_css = profile!(
"atelier.sfc.styles",
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,
macro_artifacts,
})
}