use vize_carton::{CompactString, FxHashMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacroLifecycle {
AnalyzeAndErase,
Expand,
ExtractAndErase,
}
impl MacroLifecycle {
#[inline]
pub const fn erases_from_runtime(self) -> bool {
matches!(self, Self::AnalyzeAndErase | Self::ExtractAndErase)
}
#[inline]
pub const fn produces_artifact(self) -> bool {
matches!(self, Self::ExtractAndErase)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MacroDefinition {
pub name: &'static str,
pub lifecycle: MacroLifecycle,
pub artifact_kind: Option<&'static str>,
}
pub static BUILTIN_MACROS: &[&str] = &[
"defineProps",
"defineEmits",
"defineExpose",
"defineOptions",
"defineSlots",
"defineModel",
"withDefaults",
];
pub static ECOSYSTEM_COMPILE_TIME_MACROS: &[MacroDefinition] = &[
MacroDefinition {
name: "definePage",
lifecycle: MacroLifecycle::ExtractAndErase,
artifact_kind: Some("vue-router.definePage"),
},
MacroDefinition {
name: "definePageMeta",
lifecycle: MacroLifecycle::ExtractAndErase,
artifact_kind: Some("nuxt.definePageMeta"),
},
MacroDefinition {
name: "defineRouteRules",
lifecycle: MacroLifecycle::ExtractAndErase,
artifact_kind: Some("nuxt.defineRouteRules"),
},
MacroDefinition {
name: "defineLazyHydrationComponent",
lifecycle: MacroLifecycle::Expand,
artifact_kind: None,
},
];
#[inline]
pub fn is_builtin_macro(name: &str) -> bool {
BUILTIN_MACROS.contains(&name)
}
#[inline]
pub fn ecosystem_macro_definition(name: &str) -> Option<&'static MacroDefinition> {
ECOSYSTEM_COMPILE_TIME_MACROS
.iter()
.find(|definition| definition.name == name)
}
#[inline]
pub fn is_ecosystem_compile_time_macro(name: &str) -> bool {
ecosystem_macro_definition(name).is_some()
}
#[inline]
pub fn macro_lifecycle(name: &str) -> Option<MacroLifecycle> {
if is_builtin_macro(name) {
Some(MacroLifecycle::AnalyzeAndErase)
} else {
ecosystem_macro_definition(name).map(|definition| definition.lifecycle)
}
}
#[inline]
pub fn is_compile_time_macro(name: &str) -> bool {
macro_lifecycle(name).is_some()
}
#[inline]
pub fn is_runtime_erased_macro(name: &str) -> bool {
macro_lifecycle(name).is_some_and(MacroLifecycle::erases_from_runtime)
}
#[inline]
pub fn macro_artifact_kind(name: &str) -> Option<&'static str> {
ecosystem_macro_definition(name).and_then(|definition| definition.artifact_kind)
}
#[inline]
pub fn is_artifact_macro(name: &str) -> bool {
macro_lifecycle(name).is_some_and(MacroLifecycle::produces_artifact)
}
#[inline]
pub fn runtime_erased_macro_names() -> impl Iterator<Item = &'static str> {
BUILTIN_MACROS.iter().copied().chain(
ECOSYSTEM_COMPILE_TIME_MACROS
.iter()
.filter(|definition| definition.lifecycle.erases_from_runtime())
.map(|definition| definition.name),
)
}
#[inline]
pub fn artifact_macro_names() -> impl Iterator<Item = &'static str> {
ECOSYSTEM_COMPILE_TIME_MACROS
.iter()
.filter(|definition| definition.lifecycle.produces_artifact())
.map(|definition| definition.name)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct MacroCallId(u32);
impl MacroCallId {
#[inline(always)]
pub const fn new(id: u32) -> Self {
Self(id)
}
#[inline(always)]
pub const fn as_u32(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum MacroKind {
DefineProps = 0,
DefineEmits = 1,
DefineExpose = 2,
DefineOptions = 3,
DefineSlots = 4,
DefineModel = 5,
WithDefaults = 6,
Custom = 255,
}
impl MacroKind {
#[inline]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"defineProps" => Some(Self::DefineProps),
"defineEmits" => Some(Self::DefineEmits),
"defineExpose" => Some(Self::DefineExpose),
"defineOptions" => Some(Self::DefineOptions),
"defineSlots" => Some(Self::DefineSlots),
"defineModel" => Some(Self::DefineModel),
"withDefaults" => Some(Self::WithDefaults),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct MacroCall {
pub id: MacroCallId,
pub name: CompactString,
pub kind: MacroKind,
pub start: u32,
pub end: u32,
pub runtime_args: Option<CompactString>,
pub type_args: Option<CompactString>,
}
#[derive(Debug, Clone)]
pub struct PropsDestructureBinding {
pub local: CompactString,
pub default: Option<CompactString>,
}
#[derive(Debug, Clone, Default)]
pub struct PropsDestructuredBindings {
pub bindings: FxHashMap<CompactString, PropsDestructureBinding>,
pub rest_id: Option<CompactString>,
}
impl PropsDestructuredBindings {
#[inline]
pub fn is_empty(&self) -> bool {
self.bindings.is_empty() && self.rest_id.is_none()
}
#[inline]
pub fn insert(
&mut self,
key: CompactString,
local: CompactString,
default: Option<CompactString>,
) {
self.bindings
.insert(key, PropsDestructureBinding { local, default });
}
#[inline]
pub fn get(&self, key: &str) -> Option<&PropsDestructureBinding> {
self.bindings.get(key)
}
}
#[derive(Debug, Clone)]
pub struct PropDefinition {
pub name: CompactString,
pub prop_type: Option<CompactString>,
pub required: bool,
pub default_value: Option<CompactString>,
}
#[derive(Debug, Clone)]
pub struct EmitDefinition {
pub name: CompactString,
pub payload_type: Option<CompactString>,
}
#[derive(Debug, Clone)]
pub struct EmitCall {
pub event_name: CompactString,
pub is_dynamic: bool,
pub start: u32,
pub end: u32,
}
#[derive(Debug, Clone)]
pub struct ModelDefinition {
pub name: CompactString,
pub local_name: CompactString,
pub model_type: Option<CompactString>,
pub required: bool,
pub default_value: Option<CompactString>,
}
#[derive(Debug, Clone)]
pub struct TopLevelAwait {
pub start: u32,
pub end: u32,
pub expression: CompactString,
}
#[derive(Debug, Clone)]
pub struct ExposeDefinition {
pub name: CompactString,
pub expose_type: Option<CompactString>,
}
#[derive(Debug, Clone)]
pub struct SlotsDefinition {
pub name: CompactString,
pub props_type: Option<CompactString>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacroBindingKind {
Prop,
PropAliased,
Rest,
}
#[derive(Debug, Default)]
pub struct MacroTracker {
calls: Vec<MacroCall>,
props: Vec<PropDefinition>,
emits: Vec<EmitDefinition>,
emit_calls: Vec<EmitCall>,
models: Vec<ModelDefinition>,
exposes: Vec<ExposeDefinition>,
slots: Vec<SlotsDefinition>,
props_destructure: Option<PropsDestructuredBindings>,
top_level_awaits: Vec<TopLevelAwait>,
next_id: u32,
define_props_idx: Option<usize>,
define_emits_idx: Option<usize>,
define_expose_idx: Option<usize>,
define_slots_idx: Option<usize>,
}
impl MacroTracker {
#[inline]
pub fn new() -> Self {
Self::default()
}
pub fn add_call(
&mut self,
name: impl Into<CompactString>,
kind: MacroKind,
start: u32,
end: u32,
runtime_args: Option<CompactString>,
type_args: Option<CompactString>,
) -> MacroCallId {
let id = MacroCallId::new(self.next_id);
self.next_id += 1;
let idx = self.calls.len();
match kind {
MacroKind::DefineProps => self.define_props_idx = Some(idx),
MacroKind::DefineEmits => self.define_emits_idx = Some(idx),
MacroKind::DefineExpose => self.define_expose_idx = Some(idx),
MacroKind::DefineSlots => self.define_slots_idx = Some(idx),
_ => {}
}
self.calls.push(MacroCall {
id,
name: name.into(),
kind,
start,
end,
runtime_args,
type_args,
});
id
}
#[inline]
pub fn all_calls(&self) -> &[MacroCall] {
&self.calls
}
#[inline]
pub fn define_props(&self) -> Option<&MacroCall> {
self.define_props_idx.map(|idx| &self.calls[idx])
}
#[inline]
pub fn define_emits(&self) -> Option<&MacroCall> {
self.define_emits_idx.map(|idx| &self.calls[idx])
}
#[inline]
pub fn define_expose(&self) -> Option<&MacroCall> {
self.define_expose_idx.map(|idx| &self.calls[idx])
}
#[inline]
pub fn define_slots(&self) -> Option<&MacroCall> {
self.define_slots_idx.map(|idx| &self.calls[idx])
}
#[inline]
pub fn add_prop(&mut self, prop: PropDefinition) {
self.props.push(prop);
}
#[inline]
pub fn props(&self) -> &[PropDefinition] {
&self.props
}
#[inline]
pub fn add_emit(&mut self, emit: EmitDefinition) {
self.emits.push(emit);
}
#[inline]
pub fn emits(&self) -> &[EmitDefinition] {
&self.emits
}
#[inline]
pub fn add_emit_call(
&mut self,
event_name: CompactString,
is_dynamic: bool,
start: u32,
end: u32,
) {
self.emit_calls.push(EmitCall {
event_name,
is_dynamic,
start,
end,
});
}
#[inline]
pub fn emit_calls(&self) -> &[EmitCall] {
&self.emit_calls
}
#[inline]
pub fn is_event_emitted(&self, event_name: &str) -> bool {
self.emit_calls
.iter()
.any(|c| c.event_name.as_str() == event_name && !c.is_dynamic)
}
pub fn emit_calls_for_event<'a>(
&'a self,
event_name: &'a str,
) -> impl Iterator<Item = &'a EmitCall> + 'a {
self.emit_calls
.iter()
.filter(move |c| c.event_name.as_str() == event_name)
}
#[inline]
pub fn add_model(&mut self, model: ModelDefinition) {
self.models.push(model);
}
#[inline]
pub fn models(&self) -> &[ModelDefinition] {
&self.models
}
#[inline]
pub fn add_expose(&mut self, expose: ExposeDefinition) {
self.exposes.push(expose);
}
#[inline]
pub fn exposes(&self) -> &[ExposeDefinition] {
&self.exposes
}
#[inline]
pub fn add_slot(&mut self, slot: SlotsDefinition) {
self.slots.push(slot);
}
#[inline]
pub fn slots(&self) -> &[SlotsDefinition] {
&self.slots
}
#[inline]
pub fn set_props_destructure(&mut self, destructure: PropsDestructuredBindings) {
self.props_destructure = Some(destructure);
}
#[inline]
pub fn props_destructure(&self) -> Option<&PropsDestructuredBindings> {
self.props_destructure.as_ref()
}
#[inline]
pub fn add_top_level_await(&mut self, expression: CompactString, start: u32, end: u32) {
self.top_level_awaits.push(TopLevelAwait {
start,
end,
expression,
});
}
#[inline]
pub fn has_top_level_await(&self) -> bool {
!self.top_level_awaits.is_empty()
}
#[inline]
pub fn top_level_awaits(&self) -> &[TopLevelAwait] {
&self.top_level_awaits
}
#[inline]
pub fn is_async(&self) -> bool {
self.has_top_level_await()
}
}
#[cfg(test)]
mod tests {
use super::{
artifact_macro_names, is_artifact_macro, is_compile_time_macro,
is_ecosystem_compile_time_macro, is_runtime_erased_macro, macro_artifact_kind,
macro_lifecycle, runtime_erased_macro_names, MacroKind, MacroLifecycle, MacroTracker,
};
use vize_carton::CompactString;
#[test]
fn test_macro_lifecycle_classifies_builtin_and_ecosystem_macros() {
assert_eq!(
macro_lifecycle("defineProps"),
Some(MacroLifecycle::AnalyzeAndErase)
);
assert_eq!(
macro_lifecycle("definePage"),
Some(MacroLifecycle::ExtractAndErase)
);
assert_eq!(
macro_lifecycle("definePageMeta"),
Some(MacroLifecycle::ExtractAndErase)
);
assert_eq!(
macro_lifecycle("defineRouteRules"),
Some(MacroLifecycle::ExtractAndErase)
);
assert_eq!(
macro_lifecycle("defineLazyHydrationComponent"),
Some(MacroLifecycle::Expand)
);
assert_eq!(macro_lifecycle("notAMacro"), None);
assert!(is_compile_time_macro("definePage"));
assert!(is_ecosystem_compile_time_macro("definePage"));
assert!(is_runtime_erased_macro("definePage"));
assert!(is_artifact_macro("definePage"));
assert_eq!(
macro_artifact_kind("definePage"),
Some("vue-router.definePage")
);
assert!(is_compile_time_macro("definePageMeta"));
assert!(is_ecosystem_compile_time_macro("definePageMeta"));
assert!(is_runtime_erased_macro("definePageMeta"));
assert!(is_artifact_macro("definePageMeta"));
assert_eq!(
macro_artifact_kind("definePageMeta"),
Some("nuxt.definePageMeta")
);
assert!(is_compile_time_macro("defineRouteRules"));
assert!(is_ecosystem_compile_time_macro("defineRouteRules"));
assert!(is_runtime_erased_macro("defineRouteRules"));
assert!(is_artifact_macro("defineRouteRules"));
assert_eq!(
macro_artifact_kind("defineRouteRules"),
Some("nuxt.defineRouteRules")
);
assert!(is_compile_time_macro("defineLazyHydrationComponent"));
assert!(is_ecosystem_compile_time_macro(
"defineLazyHydrationComponent"
));
assert!(!is_runtime_erased_macro("defineLazyHydrationComponent"));
assert!(!is_artifact_macro("defineLazyHydrationComponent"));
assert_eq!(macro_artifact_kind("defineLazyHydrationComponent"), None);
assert!(runtime_erased_macro_names().any(|name| name == "definePage"));
assert!(runtime_erased_macro_names().any(|name| name == "definePageMeta"));
assert!(runtime_erased_macro_names().any(|name| name == "defineRouteRules"));
assert!(!runtime_erased_macro_names().any(|name| name == "defineLazyHydrationComponent"));
assert!(artifact_macro_names().any(|name| name == "definePage"));
assert!(artifact_macro_names().any(|name| name == "definePageMeta"));
assert!(artifact_macro_names().any(|name| name == "defineRouteRules"));
assert!(!artifact_macro_names().any(|name| name == "defineLazyHydrationComponent"));
}
#[test]
fn test_macro_tracker() {
let mut tracker = MacroTracker::new();
let id = tracker.add_call(
"defineProps",
MacroKind::DefineProps,
0,
20,
None,
Some(CompactString::new("{ msg: string }")),
);
assert_eq!(id.as_u32(), 0);
assert!(tracker.define_props().is_some());
assert!(tracker.define_emits().is_none());
}
#[test]
fn test_top_level_await() {
let mut tracker = MacroTracker::new();
assert!(!tracker.is_async());
tracker.add_top_level_await(CompactString::new("fetch('/api')"), 10, 30);
assert!(tracker.is_async());
assert_eq!(tracker.top_level_awaits().len(), 1);
}
}