From 011444fbe7a909d1856241e751767405c304ea3c Mon Sep 17 00:00:00 2001 From: Marsel Shaikhin Date: Fri, 17 Oct 2025 09:26:52 +0200 Subject: [PATCH 1/9] feat: rework props handling and codegen (WIP) --- Cargo.lock | 13 + crates/fervid/benches/codegen_bench.rs | 2 + crates/fervid/src/lib.rs | 6 + .../fervid_codegen/src/directives/v_model.rs | 2 + crates/fervid_core/src/ast.rs | 228 ++++++ crates/fervid_core/src/lib.rs | 2 + crates/fervid_core/src/structs.rs | 6 + crates/fervid_core/src/vue_imports.rs | 8 + crates/fervid_transform/Cargo.toml | 1 + crates/fervid_transform/src/error.rs | 10 + crates/fervid_transform/src/lib.rs | 2 + .../src/script/resolve_type.rs | 1 + crates/fervid_transform/src/structs.rs | 33 +- .../fervid_transform/src/template/core/mod.rs | 3 + .../src/template/core/v_bind.rs | 0 .../src/template/core/v_model.rs | 0 .../src/template/core/v_on.rs | 0 .../src/template/directive_transforms.rs | 100 +++ .../fervid_transform/src/template/dom/mod.rs | 0 .../src/template/expr_transform.rs | 49 +- crates/fervid_transform/src/template/mod.rs | 5 + .../src/template/transform_element.rs | 750 ++++++++++++++++++ .../fervid_transform/src/template/v_model.rs | 76 ++ 23 files changed, 1274 insertions(+), 23 deletions(-) create mode 100644 crates/fervid_core/src/ast.rs create mode 100644 crates/fervid_transform/src/template/core/mod.rs create mode 100644 crates/fervid_transform/src/template/core/v_bind.rs create mode 100644 crates/fervid_transform/src/template/core/v_model.rs create mode 100644 crates/fervid_transform/src/template/core/v_on.rs create mode 100644 crates/fervid_transform/src/template/directive_transforms.rs create mode 100644 crates/fervid_transform/src/template/dom/mod.rs create mode 100644 crates/fervid_transform/src/template/transform_element.rs create mode 100644 crates/fervid_transform/src/template/v_model.rs diff --git a/Cargo.lock b/Cargo.lock index a84ce2e8..3c57e86f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -771,6 +771,18 @@ dependencies = [ "sourcemap", ] +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -969,6 +981,7 @@ dependencies = [ name = "fervid_transform" version = "0.2.0" dependencies = [ + "enum_dispatch", "fervid_core", "fervid_css", "flagset", diff --git a/crates/fervid/benches/codegen_bench.rs b/crates/fervid/benches/codegen_bench.rs index 27c45791..c5b214f8 100644 --- a/crates/fervid/benches/codegen_bench.rs +++ b/crates/fervid/benches/codegen_bench.rs @@ -33,6 +33,8 @@ fn codegen_benchmark(c: &mut Criterion) { transform_asset_urls: TransformAssetUrlsConfig::default(), errors: vec![], warnings: vec![], + directive_scopes: Default::default(), + directive_transforms: Default::default(), }; fervid_transform::template::transform_and_record_template(template_block, &mut ctx); diff --git a/crates/fervid/src/lib.rs b/crates/fervid/src/lib.rs index 23222664..d7191e72 100644 --- a/crates/fervid/src/lib.rs +++ b/crates/fervid/src/lib.rs @@ -155,6 +155,10 @@ pub fn compile(source: &str, options: CompileOptions) -> Result Result Result scope_id: &file_hash, filename: "anonymous.vue", transform_asset_urls: TransformAssetUrlsConfig::default(), + directive_transforms: Default::default(), }; let transform_result = transform_sfc(sfc, transform_options, &mut transform_errors); diff --git a/crates/fervid_codegen/src/directives/v_model.rs b/crates/fervid_codegen/src/directives/v_model.rs index ec2636ef..03323d92 100644 --- a/crates/fervid_codegen/src/directives/v_model.rs +++ b/crates/fervid_codegen/src/directives/v_model.rs @@ -24,6 +24,8 @@ impl CodegenContext { let span = v_model.span; let mut buf = String::new(); + // TODO Move the props part into the transform + // 1. Get bound attribute (part after `:` or `modelValue`). // `v-model="smth"` is same as `v-model:modelValue="smth"` let bound_attribute = v_model diff --git a/crates/fervid_core/src/ast.rs b/crates/fervid_core/src/ast.rs new file mode 100644 index 00000000..83105377 --- /dev/null +++ b/crates/fervid_core/src/ast.rs @@ -0,0 +1,228 @@ +// Adapted from https://github.com/vuejs/core/blob/5a8aa0b2ba575e098cbb63b396e9bcb751eb3a0f/packages/compiler-core/src/ast.ts + +use swc_core::{ + common::{Span, DUMMY_SP}, + ecma::ast::{ + Bool, CallExpr, Callee, Expr, ExprOrSpread, IdentName, KeyValueProp, Lit, Prop, PropName, + PropOrSpread, Str, + }, +}; + +use crate::{FervidAtom, VueImports}; + +#[derive(Default)] +pub enum ConstantTypes { + #[default] + NotConstant = 0, + CanSkipPatch, + CanCache, + CanStringify, +} + +pub struct SimpleExpressionNode { + pub ast: Box, + pub is_static: bool, + pub const_type: ConstantTypes, + pub is_handler_key: bool, +} + +pub struct CompoundExpressionNode { + pub ast: Box, + pub is_handler_key: bool, +} + +pub enum ExpressionNode { + SimpleExpression(SimpleExpressionNode), + CompoundExpression(CompoundExpressionNode), +} + +pub struct SimpleExpressionPropNameNode { + pub ast: IdentName, + pub is_static: bool, + pub const_type: ConstantTypes, + pub is_handler_key: bool, +} + +pub struct CompoundExpressionPropNameNode { + pub ast: PropName, + pub is_handler_key: bool, +} + +pub enum ExpressionPropNameNode { + SimpleExpression(SimpleExpressionPropNameNode), + CompoundExpression(CompoundExpressionPropNameNode), +} + +// JS Node Types + +pub enum JsChildNode { + CallExpression(Box), + ObjectExpression(Box), + ExpressionNode(Box), + OriginalValueMarker, +} + +pub struct CallExpression { + pub callee: VueImports, + pub span: Span, + pub arguments: Vec, +} + +pub struct ObjectExpression { + pub properties: Vec, + pub span: Span, +} + +pub struct Property { + pub key: ExpressionPropNameNode, + pub value: JsChildNode, +} + +pub fn create_call_expression( + callee: VueImports, + arguments: Vec, + span: Span, +) -> CallExpression { + CallExpression { + callee, + span, + arguments, + } +} + +pub fn create_object_expression(properties: Vec, span: Span) -> ObjectExpression { + ObjectExpression { properties, span } +} + +pub fn create_object_property( + key: ExpressionPropNameNode, + value: JsChildNode, +) -> Property { + Property { key, value } +} + +pub fn create_simple_expression_propname( + content: FervidAtom, + is_static: bool, + span: Span, +) -> SimpleExpressionPropNameNode { + SimpleExpressionPropNameNode { + ast: IdentName { sym: content, span }, + is_static, + const_type: Default::default(), + is_handler_key: false, + } +} + +pub fn create_simple_expression_bool(content: bool) -> SimpleExpressionNode { + SimpleExpressionNode { + ast: Box::new(Expr::Lit(Lit::Bool(Bool { + value: content, + span: DUMMY_SP, + }))), + is_static: true, + const_type: ConstantTypes::CanStringify, + is_handler_key: false, + } +} + +pub fn create_simple_expression_str( + content: FervidAtom, + is_static: bool, + span: Span, +) -> SimpleExpressionNode { + SimpleExpressionNode { + ast: Box::new(Expr::Lit(Lit::Str(Str { + span, + value: content, + raw: None, + }))), + is_static, + const_type: ConstantTypes::CanStringify, + is_handler_key: false, + } +} + +// Property + +// impl Into for Property { +// fn into(self) -> PropOrSpread { +// PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { +// key: self.key.into(), +// value: self.value.ast, +// }))) +// } +// } + +impl From for Property { + fn from(value: PropOrSpread) -> Self { + Property { + key: todo!(), + value: todo!(), + } + } +} + +// CallExpression + +impl From for CallExpression { + fn from(value: CallExpr) -> Self { + todo!() + // Self { + // callee: (), + // span: (), + // arguments: (), + // } + } +} + +// ExpressionNode + +impl From for ExpressionNode { + fn from(value: Expr) -> Self { + Self::CompoundExpression(CompoundExpressionNode { + ast: Box::new(value), + is_handler_key: false, + }) + } +} + +// ExpressionPropNameNode + +impl ExpressionPropNameNode { + pub fn is_handler_key(&self) -> bool { + match self { + ExpressionPropNameNode::SimpleExpression(s) => s.is_handler_key, + ExpressionPropNameNode::CompoundExpression(c) => c.is_handler_key, + } + } +} + +impl Into for ExpressionPropNameNode { + fn into(self) -> PropName { + match self { + ExpressionPropNameNode::SimpleExpression(s) => PropName::Ident(s.ast), + ExpressionPropNameNode::CompoundExpression(c) => c.ast, + } + } +} + +impl From for ExpressionPropNameNode { + fn from(value: SimpleExpressionPropNameNode) -> Self { + Self::SimpleExpression(value) + } +} + +// JsChildNode + +impl From for JsChildNode { + fn from(value: SimpleExpressionNode) -> Self { + Self::ExpressionNode(Box::new(ExpressionNode::SimpleExpression(value))) + } +} + +impl From for JsChildNode { + fn from(value: CallExpression) -> Self { + Self::CallExpression(Box::new(value)) + } +} \ No newline at end of file diff --git a/crates/fervid_core/src/lib.rs b/crates/fervid_core/src/lib.rs index ba8b48d4..02263d32 100644 --- a/crates/fervid_core/src/lib.rs +++ b/crates/fervid_core/src/lib.rs @@ -1,4 +1,5 @@ mod all_html_tags; +mod ast; mod bindings; pub mod error; mod sfc; @@ -9,6 +10,7 @@ mod vue_builtins; mod vue_imports; pub use all_html_tags::is_html_tag; +pub use ast::*; pub use bindings::*; pub use sfc::*; pub use structs::*; diff --git a/crates/fervid_core/src/structs.rs b/crates/fervid_core/src/structs.rs index 9eebf2c9..3ccfb974 100644 --- a/crates/fervid_core/src/structs.rs +++ b/crates/fervid_core/src/structs.rs @@ -463,3 +463,9 @@ pub enum TemplateGenerationMode { #[default] RenderFn, } + +impl TemplateGenerationMode { + pub fn is_inline(&self) -> bool { + matches!(self, TemplateGenerationMode::Inline) + } +} diff --git a/crates/fervid_core/src/vue_imports.rs b/crates/fervid_core/src/vue_imports.rs index 725d2d93..7567e991 100644 --- a/crates/fervid_core/src/vue_imports.rs +++ b/crates/fervid_core/src/vue_imports.rs @@ -23,6 +23,8 @@ flags! { DefineComponent, #[strum(serialize = "_Fragment")] Fragment, + #[strum(serialize = "_guardReactiveProps")] + GuardReactiveProps, #[strum(serialize = "_isMemoSame")] IsMemoSame, #[strum(serialize = "_isRef")] @@ -33,10 +35,14 @@ flags! { MergeDefaults, #[strum(serialize = "_mergeModels")] MergeModels, + #[strum(serialize = "_mergeProps")] + MergeProps, #[strum(serialize = "_normalizeClass")] NormalizeClass, #[strum(serialize = "_normalizeStyle")] NormalizeStyle, + #[strum(serialize = "_normalizeProps")] + NormalizeProps, #[strum(serialize = "_openBlock")] OpenBlock, #[strum(serialize = "_renderList")] @@ -59,6 +65,8 @@ flags! { ToDisplayString, #[strum(serialize = "_toHandlerKey")] ToHandlerKey, + #[strum(serialize = "_toHandlers")] + ToHandlers, #[strum(serialize = "_Transition")] Transition, #[strum(serialize = "_TransitionGroup")] diff --git a/crates/fervid_transform/Cargo.toml b/crates/fervid_transform/Cargo.toml index 3beb0a2d..4663982f 100644 --- a/crates/fervid_transform/Cargo.toml +++ b/crates/fervid_transform/Cargo.toml @@ -24,6 +24,7 @@ itertools = "*" url = "2" percent-encoding = "*" indexmap = "2.10.0" +enum_dispatch = "0.3.13" [dev-dependencies] swc_ecma_codegen = { workspace = true } diff --git a/crates/fervid_transform/src/error.rs b/crates/fervid_transform/src/error.rs index 6c25ef0d..1f444f3c 100644 --- a/crates/fervid_transform/src/error.rs +++ b/crates/fervid_transform/src/error.rs @@ -107,6 +107,16 @@ pub enum TemplateErrorKind { TransformAssetUrlsBaseUrlParseFailed, /// Failed parsing the configured base URL when doing asset URL transform TransformAssetUrlsUrlParseFailed, + /// v-model value must be a valid JavaScript member expression + VModelMalformedExpression, + /// v-model cannot be used on a prop, because local prop bindings are not writable. Use a v-bind binding combined with a v-on listener that emits update:x event instead + VModelOnProps, + /// v-model cannot be used on v-for or v-slot scope variables because they are not writable + VModelOnScopeVariable, + /// v-on is missing expression + VOnNoExpression, + /// v-slot can only be used on components or