mod external_types;
mod helpers;
mod parse;
mod props;
use crate::types::{BindingMetadata, BindingType};
use vize_carton::{CompactString, String, ToCompactString};
use vize_croquis::analysis::Croquis;
use vize_croquis::macros::{EmitDefinition, ModelDefinition, PropDefinition};
use super::ScriptSetupMacros;
#[derive(Debug)]
pub struct ScriptCompileContext {
pub source: String,
pub bindings: BindingMetadata,
pub macros: ScriptSetupMacros,
pub has_define_props_call: bool,
pub has_define_emits_call: bool,
pub has_define_expose_call: bool,
pub has_define_options_call: bool,
pub has_define_slots_call: bool,
pub has_define_model_call: bool,
pub emits_runtime_decl: Option<String>,
pub emits_type_decl: Option<String>,
pub emit_decl_id: Option<String>,
pub interfaces: vize_carton::FxHashMap<String, String>,
pub type_aliases: vize_carton::FxHashMap<String, String>,
}
impl ScriptCompileContext {
pub fn new(source: &str) -> Self {
Self {
source: source.to_compact_string(),
bindings: BindingMetadata::default(),
macros: ScriptSetupMacros::default(),
has_define_props_call: false,
has_define_emits_call: false,
has_define_expose_call: false,
has_define_options_call: false,
has_define_slots_call: false,
has_define_model_call: false,
emits_runtime_decl: None,
emits_type_decl: None,
emit_decl_id: None,
interfaces: vize_carton::FxHashMap::default(),
type_aliases: vize_carton::FxHashMap::default(),
}
}
pub fn analyze(&mut self) {
let source = std::mem::take(&mut self.source);
self.parse_with_oxc(&source);
self.source = source;
self.bindings.is_script_setup = true;
}
pub fn to_analysis_summary(&self) -> Croquis {
let mut summary = Croquis::new();
summary.bindings.is_script_setup = true;
for (name, binding_type) in &self.bindings.bindings {
summary.bindings.add(name.as_str(), *binding_type);
}
for (local, key) in &self.bindings.props_aliases {
summary
.bindings
.props_aliases
.insert(CompactString::new(local), CompactString::new(key));
}
if let Some(ref props_call) = self.macros.define_props {
for (name, binding_type) in &self.bindings.bindings {
if matches!(binding_type, BindingType::Props) {
summary.macros.add_prop(PropDefinition {
name: CompactString::new(name),
required: false, prop_type: None,
default_value: props_call.binding_name.clone().map(CompactString::new),
});
}
}
}
if let Some(ref emits_call) = self.macros.define_emits {
let trimmed = emits_call.args.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
let inner = &trimmed[1..trimmed.len() - 1];
for part in inner.split(',') {
let part = part.trim();
if (part.starts_with('\'') && part.ends_with('\''))
|| (part.starts_with('"') && part.ends_with('"'))
{
let name = &part[1..part.len() - 1];
summary.macros.add_emit(EmitDefinition {
name: CompactString::new(name),
payload_type: None,
});
}
}
}
}
for model_call in &self.macros.define_models {
if let Some(ref binding_name) = model_call.binding_name {
let args = model_call.args.trim();
let name = if args.starts_with('\'') || args.starts_with('"') {
let quote = args.as_bytes()[0];
if let Some(end) = args[1..].find(|c: char| c as u8 == quote) {
CompactString::new(&args[1..=end])
} else {
CompactString::new("modelValue")
}
} else {
CompactString::new("modelValue")
};
summary.macros.add_model(ModelDefinition {
name: name.clone(),
local_name: CompactString::new(binding_name),
model_type: None,
required: false,
default_value: None,
});
}
}
summary
}
pub fn extract_all_macros(&mut self) {
let source = std::mem::take(&mut self.source);
self.parse_with_oxc(&source);
self.source = source;
}
}
#[cfg(test)]
mod tests {
use super::ScriptCompileContext;
use crate::types::BindingType;
use vize_carton::ToCompactString;
#[test]
fn test_context_analyze() {
let content = r#"
const msg = ref('hello')
const count = ref(0)
let name = 'world'
const double = computed(() => count.value * 2)
function increment() { count.value++ }
"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert_eq!(
ctx.bindings.bindings.get("msg"),
Some(&BindingType::SetupRef)
);
assert_eq!(
ctx.bindings.bindings.get("count"),
Some(&BindingType::SetupRef)
);
assert_eq!(
ctx.bindings.bindings.get("name"),
Some(&BindingType::SetupLet)
);
assert_eq!(
ctx.bindings.bindings.get("increment"),
Some(&BindingType::SetupConst)
);
}
#[test]
fn test_extract_define_props_typed() {
let content = r#"const props = defineProps<{ msg: string }>()"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert!(ctx.has_define_props_call);
assert!(ctx.macros.define_props.is_some());
let props_call = ctx.macros.define_props.unwrap();
assert_eq!(
props_call.type_args,
Some("{ msg: string }".to_compact_string())
);
}
#[test]
fn test_extract_define_emits_typed() {
let content = r#"const emit = defineEmits<{ (e: 'click'): void }>()"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert!(ctx.has_define_emits_call);
assert!(ctx.macros.define_emits.is_some());
}
#[test]
fn test_extract_with_defaults() {
let content =
r#"const props = withDefaults(defineProps<{ msg?: string }>(), { msg: 'hello' })"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert!(ctx.has_define_props_call);
assert!(ctx.macros.with_defaults.is_some());
}
#[test]
fn test_props_destructure() {
let content = r#"const { foo, bar } = defineProps<{ foo: string, bar: number }>()"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert!(ctx.macros.props_destructure.is_some());
let destructure = ctx.macros.props_destructure.as_ref().unwrap();
assert_eq!(destructure.bindings.len(), 2);
assert!(destructure.bindings.contains_key("foo"));
assert!(destructure.bindings.contains_key("bar"));
}
#[test]
fn test_props_destructure_with_alias() {
let content =
r#"const { foo: myFoo, bar = 123 } = defineProps<{ foo: string, bar?: number }>()"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert!(ctx.macros.props_destructure.is_some());
let destructure = ctx.macros.props_destructure.as_ref().unwrap();
assert!(destructure.bindings.contains_key("foo"));
assert!(destructure.bindings.contains_key("bar"));
assert_eq!(destructure.bindings.get("foo").unwrap().local, "myFoo");
assert_eq!(destructure.bindings.get("bar").unwrap().local, "bar");
assert!(destructure.bindings.get("bar").unwrap().default.is_some());
}
#[test]
fn test_define_props_with_interface_reference() {
let content = r#"
interface Props {
msg: string
count?: number
}
const props = defineProps<Props>()
"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert!(ctx.interfaces.contains_key("Props"));
assert!(ctx.has_define_props_call);
assert_eq!(ctx.bindings.bindings.get("msg"), Some(&BindingType::Props));
assert_eq!(
ctx.bindings.bindings.get("count"),
Some(&BindingType::Props)
);
}
#[test]
fn test_define_props_with_type_alias_reference() {
let content = r#"
type Props = {
foo: string
bar: number
}
const props = defineProps<Props>()
"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert!(ctx.type_aliases.contains_key("Props"));
assert!(ctx.has_define_props_call);
assert_eq!(ctx.bindings.bindings.get("foo"), Some(&BindingType::Props));
assert_eq!(ctx.bindings.bindings.get("bar"), Some(&BindingType::Props));
}
#[test]
fn test_define_props_with_exported_type_alias() {
let content = r#"
export type MenuItemProps = {
id: string
label: string
routeName: string
disabled?: boolean
}
const { label, disabled, routeName } = defineProps<MenuItemProps>()
"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert!(
ctx.type_aliases.contains_key("MenuItemProps"),
"export type alias should be collected"
);
assert!(ctx.has_define_props_call);
assert_eq!(
ctx.bindings.bindings.get("label"),
Some(&BindingType::Props)
);
assert_eq!(
ctx.bindings.bindings.get("disabled"),
Some(&BindingType::Props)
);
assert_eq!(
ctx.bindings.bindings.get("routeName"),
Some(&BindingType::Props)
);
}
#[test]
fn test_define_props_with_exported_interface() {
let content = r#"
export interface Props {
msg: string
count?: number
}
const props = defineProps<Props>()
"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert!(
ctx.interfaces.contains_key("Props"),
"export interface should be collected"
);
assert!(ctx.has_define_props_call);
assert_eq!(ctx.bindings.bindings.get("msg"), Some(&BindingType::Props));
assert_eq!(
ctx.bindings.bindings.get("count"),
Some(&BindingType::Props)
);
}
#[test]
fn test_with_defaults_with_interface() {
let content = r#"
interface Props {
msg?: string
count?: number
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
count: 0
})
"#;
let mut ctx = ScriptCompileContext::new(content);
ctx.analyze();
assert!(ctx.has_define_props_call);
assert!(ctx.macros.with_defaults.is_some());
assert_eq!(ctx.bindings.bindings.get("msg"), Some(&BindingType::Props));
assert_eq!(
ctx.bindings.bindings.get("count"),
Some(&BindingType::Props)
);
}
}