diff --git a/crates/fervid/src/lib.rs b/crates/fervid/src/lib.rs index ef6bf27..cf83333 100644 --- a/crates/fervid/src/lib.rs +++ b/crates/fervid/src/lib.rs @@ -22,6 +22,8 @@ //! let mut transform_errors = Vec::new(); //! let transform_options = fervid_transform::TransformSfcOptions { //! is_prod: true, +//! is_ce: false, +//! props_destructure: fervid_transform::PropsDestructureConfig::default(), //! scope_id: "filehash", //! filename: "input.vue" //! }; @@ -58,7 +60,7 @@ use fervid_codegen::CodegenContext; pub use fervid_core::*; use fervid_parser::SfcParser; use fervid_transform::{ - style::should_transform_style_block, transform_sfc, SetupBinding, TransformSfcOptions, + style::should_transform_style_block, transform_sfc, PropsDestructureConfig, SetupBinding, TransformSfcOptions }; use fxhash::FxHasher32; use std::{ @@ -78,7 +80,9 @@ pub struct CompileOptions<'o> { // pub scoped: Option, // pub slotted: Option, pub is_prod: Option, + pub is_custom_element: Option, pub ssr: Option, + pub props_destructure: Option, // pub ssrCssVars?: string[], // pub inMap?: RawSourceMap, // pub compiler?: TemplateCompiler, @@ -131,6 +135,7 @@ pub fn compile(source: &str, options: CompileOptions) -> Result Result Result let mut transform_errors = Vec::new(); let transform_options = TransformSfcOptions { is_prod, + is_ce: false, + props_destructure: PropsDestructureConfig::default(), scope_id: &file_hash, filename: "anonymous.vue".into(), }; diff --git a/crates/fervid_codegen/src/attributes/mod.rs b/crates/fervid_codegen/src/attributes/mod.rs index 42785fa..59dddda 100644 --- a/crates/fervid_codegen/src/attributes/mod.rs +++ b/crates/fervid_codegen/src/attributes/mod.rs @@ -1,6 +1,6 @@ use fervid_core::{ - fervid_atom, AttributeOrBinding, FervidAtom, IntoIdent, StrOrExpr, VBindDirective, - VOnDirective, VueImports, + fervid_atom, str_to_propname, AttributeOrBinding, FervidAtom, IntoIdent, StrOrExpr, + VBindDirective, VOnDirective, VueImports, }; use regex::Regex; use swc_core::{ @@ -12,7 +12,7 @@ use swc_core::{ }, }; -use crate::{context::CodegenContext, utils::str_to_propname}; +use crate::context::CodegenContext; lazy_static! { static ref CSS_RE: Regex = diff --git a/crates/fervid_codegen/src/builtins/component.rs b/crates/fervid_codegen/src/builtins/component.rs index 9757479..96a79c2 100644 --- a/crates/fervid_codegen/src/builtins/component.rs +++ b/crates/fervid_codegen/src/builtins/component.rs @@ -212,7 +212,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"(_openBlock(),_createBlock(_resolveDynamicComponent("div"),null,{"default":_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#, + r#"(_openBlock(),_createBlock(_resolveDynamicComponent("div"),null,{default:_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#, ) } @@ -300,7 +300,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"(_openBlock(),_createBlock(_resolveDynamicComponent("div"),{foo:"bar",baz:qux},{named:_withCtx(()=>[_createTextVNode("bazqux")]),"default":_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#, + r#"(_openBlock(),_createBlock(_resolveDynamicComponent("div"),{foo:"bar",baz:qux},{named:_withCtx(()=>[_createTextVNode("bazqux")]),default:_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#, ) } diff --git a/crates/fervid_codegen/src/builtins/suspense.rs b/crates/fervid_codegen/src/builtins/suspense.rs index bbd6d81..5f387a4 100644 --- a/crates/fervid_codegen/src/builtins/suspense.rs +++ b/crates/fervid_codegen/src/builtins/suspense.rs @@ -100,7 +100,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"(_openBlock(),_createBlock(_Suspense,null,{"default":_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#, + r#"(_openBlock(),_createBlock(_Suspense,null,{default:_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#, ) } @@ -123,7 +123,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"(_openBlock(),_createBlock(_Suspense,{foo:"bar",baz:qux},{"default":_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#, + r#"(_openBlock(),_createBlock(_Suspense,{foo:"bar",baz:qux},{default:_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#, ) } diff --git a/crates/fervid_codegen/src/builtins/transition.rs b/crates/fervid_codegen/src/builtins/transition.rs index a86cb59..7aa3741 100644 --- a/crates/fervid_codegen/src/builtins/transition.rs +++ b/crates/fervid_codegen/src/builtins/transition.rs @@ -99,7 +99,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"_createVNode(_Transition,null,{"default":_withCtx(()=>[_createTextVNode("foobar")]),_:1})"#, + r#"_createVNode(_Transition,null,{default:_withCtx(()=>[_createTextVNode("foobar")]),_:1})"#, ) } @@ -122,7 +122,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"_createVNode(_Transition,{foo:"bar",baz:qux},{"default":_withCtx(()=>[_createTextVNode("foobar")]),_:1})"#, + r#"_createVNode(_Transition,{foo:"bar",baz:qux},{default:_withCtx(()=>[_createTextVNode("foobar")]),_:1})"#, ) } diff --git a/crates/fervid_codegen/src/builtins/transition_group.rs b/crates/fervid_codegen/src/builtins/transition_group.rs index 1665b3a..5669023 100644 --- a/crates/fervid_codegen/src/builtins/transition_group.rs +++ b/crates/fervid_codegen/src/builtins/transition_group.rs @@ -99,7 +99,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"_createVNode(_TransitionGroup,null,{"default":_withCtx(()=>[_createTextVNode("foobar")]),_:1})"#, + r#"_createVNode(_TransitionGroup,null,{default:_withCtx(()=>[_createTextVNode("foobar")]),_:1})"#, ) } @@ -122,7 +122,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"_createVNode(_TransitionGroup,{foo:"bar",baz:qux},{"default":_withCtx(()=>[_createTextVNode("foobar")]),_:1})"#, + r#"_createVNode(_TransitionGroup,{foo:"bar",baz:qux},{default:_withCtx(()=>[_createTextVNode("foobar")]),_:1})"#, ) } diff --git a/crates/fervid_codegen/src/components/mod.rs b/crates/fervid_codegen/src/components/mod.rs index cb4bd1a..12cb277 100644 --- a/crates/fervid_codegen/src/components/mod.rs +++ b/crates/fervid_codegen/src/components/mod.rs @@ -1,6 +1,6 @@ use fervid_core::{ - fervid_atom, ComponentBinding, ElementNode, FervidAtom, Node, PatchHints, StartingTag, - StrOrExpr, VSlotDirective, VueDirectives, VueImports, + fervid_atom, str_or_expr_to_propname, ComponentBinding, ElementNode, FervidAtom, Node, + PatchHints, StartingTag, StrOrExpr, VSlotDirective, VueDirectives, VueImports, }; use swc_core::{ common::{Span, DUMMY_SP}, @@ -11,9 +11,7 @@ use swc_core::{ }, }; -use crate::{ - context::CodegenContext, control_flow::SlottedIterator, utils::str_or_expr_to_propname, -}; +use crate::{context::CodegenContext, control_flow::SlottedIterator}; impl CodegenContext { pub fn generate_component_vnode( @@ -741,7 +739,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"_createVNode(_component_test_component,null,{"default":_withCtx(()=>[_createTextVNode("hello from component"),_createElementVNode("div",null,"hello from div")])})"#, + r#"_createVNode(_component_test_component,null,{default:_withCtx(()=>[_createTextVNode("hello from component"),_createElementVNode("div",null,"hello from div")])})"#, false, ); @@ -792,7 +790,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"_createVNode(_component_test_component,null,{"default":_withCtx(()=>[_createTextVNode("hello from component"),_createElementVNode("div",null,"hello from div")])})"#, + r#"_createVNode(_component_test_component,null,{default:_withCtx(()=>[_createTextVNode("hello from component"),_createElementVNode("div",null,"hello from div")])})"#, false, ); } @@ -985,7 +983,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"_createVNode(_component_test_component,null,{"foo-bar":_withCtx(()=>[_createTextVNode("hello from slot")]),"default":_withCtx(()=>[_createTextVNode("hello from component"),_createElementVNode("div",null,"hello from div")])})"#, + r#"_createVNode(_component_test_component,null,{"foo-bar":_withCtx(()=>[_createTextVNode("hello from slot")]),default:_withCtx(()=>[_createTextVNode("hello from component"),_createElementVNode("div",null,"hello from div")])})"#, false, ); @@ -1057,7 +1055,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"_createVNode(_component_test_component,null,{"default":_withCtx(()=>[_createTextVNode("hello from default"),_createElementVNode("div",null,"hello from div")]),"foo-bar":_withCtx(()=>[_createTextVNode("hello from slot")])})"#, + r#"_createVNode(_component_test_component,null,{default:_withCtx(()=>[_createTextVNode("hello from default"),_createElementVNode("div",null,"hello from div")]),"foo-bar":_withCtx(()=>[_createTextVNode("hello from slot")])})"#, false, ); @@ -1110,7 +1108,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"_createVNode(_component_test_component,null,{"foo-bar":_withCtx(()=>[_createTextVNode("hello from slot")]),"default":_withCtx(()=>[_createTextVNode("hello from component"),_createElementVNode("div",null,"hello from div")])})"#, + r#"_createVNode(_component_test_component,null,{"foo-bar":_withCtx(()=>[_createTextVNode("hello from slot")]),default:_withCtx(()=>[_createTextVNode("hello from component"),_createElementVNode("div",null,"hello from div")])})"#, false, ); } @@ -1204,7 +1202,7 @@ mod tests { patch_hints: Default::default(), span: DUMMY_SP, }, - r#"_createVNode(_component_test_component,null,{"foo-bar":_withCtx(()=>[_createTextVNode("hello from slot")]),"default":_withCtx(()=>[_createTextVNode("hello from default"),_createElementVNode("div",null,"hello from div")]),baz:_withCtx(()=>[_createTextVNode("hello from baz")])})"#, + r#"_createVNode(_component_test_component,null,{"foo-bar":_withCtx(()=>[_createTextVNode("hello from slot")]),default:_withCtx(()=>[_createTextVNode("hello from default"),_createElementVNode("div",null,"hello from div")]),baz:_withCtx(()=>[_createTextVNode("hello from baz")])})"#, false, ); } diff --git a/crates/fervid_codegen/src/directives/mod.rs b/crates/fervid_codegen/src/directives/mod.rs index 32b7dc3..c1f9583 100644 --- a/crates/fervid_codegen/src/directives/mod.rs +++ b/crates/fervid_codegen/src/directives/mod.rs @@ -1,4 +1,4 @@ -use fervid_core::{CustomDirectiveBinding, FervidAtom, StrOrExpr, VueDirectives, VueImports}; +use fervid_core::{str_to_propname, CustomDirectiveBinding, FervidAtom, StrOrExpr, VueDirectives, VueImports}; use swc_core::{ common::{Span, DUMMY_SP}, ecma::ast::{ @@ -7,7 +7,7 @@ use swc_core::{ }, }; -use crate::{utils::str_to_propname, CodegenContext}; +use crate::CodegenContext; mod v_for; mod v_html; diff --git a/crates/fervid_codegen/src/directives/v_model.rs b/crates/fervid_codegen/src/directives/v_model.rs index 2ed0a2c..6c7678a 100644 --- a/crates/fervid_codegen/src/directives/v_model.rs +++ b/crates/fervid_codegen/src/directives/v_model.rs @@ -1,4 +1,6 @@ -use fervid_core::{FervidAtom, StrOrExpr, VModelDirective}; +use fervid_core::{ + str_or_expr_to_propname, str_to_propname, FervidAtom, StrOrExpr, VModelDirective, +}; use swc_core::{ common::Span, ecma::ast::{ @@ -7,10 +9,7 @@ use swc_core::{ }, }; -use crate::{ - context::CodegenContext, - utils::{str_or_expr_to_propname, str_to_propname, to_camelcase}, -}; +use crate::{context::CodegenContext, utils::to_camelcase}; impl CodegenContext { /// Generates the `v-model` parts for a component: @@ -40,7 +39,8 @@ impl CodegenContext { })))); // 3. Generate event handler propname - let event_handler_propname = generate_v_model_handler_propname(&bound_attribute, &mut buf, span); + let event_handler_propname = + generate_v_model_handler_propname(&bound_attribute, &mut buf, span); // 4. Push the update code, // e.g. `v-model="smth"` -> `"onUpdate:modelValue": $event => ((_ctx.smth) = $event)` @@ -109,7 +109,11 @@ impl CodegenContext { /// Generates the `v-model` for an element. /// This generates the update handler - pub fn generate_v_model_for_element(&self, v_model: &VModelDirective, out: &mut Vec) { + pub fn generate_v_model_for_element( + &self, + v_model: &VModelDirective, + out: &mut Vec, + ) { let span = v_model.span; let mut buf = String::new(); @@ -121,7 +125,8 @@ impl CodegenContext { .unwrap_or_else(|| "modelValue".into()); // 2. Generate event handler propname - let event_handler_propname = generate_v_model_handler_propname(&bound_attribute, &mut buf, span); + let event_handler_propname = + generate_v_model_handler_propname(&bound_attribute, &mut buf, span); // 3. Push the update handler code, // e.g. `v-model="smth"` -> `"onUpdate:modelValue": $event => ((_ctx.smth) = $event)` @@ -141,7 +146,11 @@ impl CodegenContext { /// ## Examples /// - `v-model="smth"` -> `"onUpdate:modelValue"`; /// - `v-model:users-argument="smth"` -> `"onUpdate:usersArgument"`. -fn generate_v_model_handler_propname(bound_attribute: &StrOrExpr, buf: &mut String, span: Span) -> PropName { +fn generate_v_model_handler_propname( + bound_attribute: &StrOrExpr, + buf: &mut String, + span: Span, +) -> PropName { match bound_attribute { StrOrExpr::Str(ref s) => { buf.reserve(9 + s.len()); diff --git a/crates/fervid_codegen/src/utils/mod.rs b/crates/fervid_codegen/src/utils/mod.rs index 72fb61e..f216bf3 100644 --- a/crates/fervid_codegen/src/utils/mod.rs +++ b/crates/fervid_codegen/src/utils/mod.rs @@ -1,61 +1,4 @@ -// to_ident_or_str - -use std::fmt::{Write, Error}; - -use fervid_core::{FervidAtom, StrOrExpr}; -use swc_core::{common::Span, ecma::ast::{ComputedPropName, Ident, EsReserved, IdentName, PropName, Str}}; - -/// Adapted from SWC Ident::verify_symbol -#[inline] -pub fn is_valid_ident(s: &str) -> bool { - if s.is_reserved() || s.is_reserved_in_strict_mode(true) || s.is_reserved_in_strict_bind() { - return false; - } - - let mut chars = s.chars(); - - if let Some(first) = chars.next() { - if Ident::is_valid_start(first) && chars.all(Ident::is_valid_continue) { - return true; - } - } - - false -} - -pub fn str_to_propname(s: &str, span: Span) -> PropName { - if is_valid_ident(s) { - PropName::Ident(IdentName { span, sym: s.into() }) - } else { - PropName::Str(Str { - span, - value: s.into(), - raw: None, - }) - } -} - -pub fn atom_to_propname(sym: FervidAtom, span: Span) -> PropName { - if is_valid_ident(&sym) { - PropName::Ident(IdentName { span, sym }) - } else { - PropName::Str(Str { - span, - value: sym, - raw: None, - }) - } -} - -pub fn str_or_expr_to_propname(str_or_expr: StrOrExpr, span: Span) -> PropName { - match str_or_expr { - StrOrExpr::Str(sym) => atom_to_propname(sym, span), - StrOrExpr::Expr(expr) => PropName::Computed(ComputedPropName { - span, - expr, - }), - } -} +use std::fmt::{Error, Write}; pub fn to_camelcase(s: &str, buf: &mut impl Write) -> Result<(), Error> { for (idx, word) in s.split('-').enumerate() { diff --git a/crates/fervid_core/src/utils.rs b/crates/fervid_core/src/utils.rs index aee04ca..c65283c 100644 --- a/crates/fervid_core/src/utils.rs +++ b/crates/fervid_core/src/utils.rs @@ -1,4 +1,6 @@ -use crate::{AttributeOrBinding, VBindDirective, StrOrExpr}; +use swc_core::{common::Span, ecma::ast::{ComputedPropName, EsReserved, Ident, IdentName, PropName, Str}}; + +use crate::{AttributeOrBinding, FervidAtom, StrOrExpr, VBindDirective}; /// Checks whether the attributes name is the same as `expected_name` #[inline] @@ -9,3 +11,60 @@ pub fn check_attribute_name(attr: &AttributeOrBinding, expected_name: &str) -> b if name == expected_name ) } + +/// Adapted from SWC Ident::verify_symbol +#[inline] +pub fn is_valid_ident(s: &str) -> bool { + if s.is_reserved() || s.is_reserved_in_strict_mode(true) || s.is_reserved_in_strict_bind() { + return false; + } + + is_valid_propname(s) +} + +#[inline] +pub fn is_valid_propname(s: &str) -> bool { + let mut chars = s.chars(); + + if let Some(first) = chars.next() { + if Ident::is_valid_start(first) && chars.all(Ident::is_valid_continue) { + return true; + } + } + + false +} + +pub fn str_to_propname(s: &str, span: Span) -> PropName { + if is_valid_propname(s) { + PropName::Ident(IdentName { span, sym: s.into() }) + } else { + PropName::Str(Str { + span, + value: s.into(), + raw: None, + }) + } +} + +pub fn atom_to_propname(sym: FervidAtom, span: Span) -> PropName { + if is_valid_propname(&sym) { + PropName::Ident(IdentName { span, sym }) + } else { + PropName::Str(Str { + span, + value: sym, + raw: None, + }) + } +} + +pub fn str_or_expr_to_propname(str_or_expr: StrOrExpr, span: Span) -> PropName { + match str_or_expr { + StrOrExpr::Str(sym) => atom_to_propname(sym, span), + StrOrExpr::Expr(expr) => PropName::Computed(ComputedPropName { + span, + expr, + }), + } +} diff --git a/crates/fervid_farmfe/src/lib.rs b/crates/fervid_farmfe/src/lib.rs index 20c2d21..d04b41b 100644 --- a/crates/fervid_farmfe/src/lib.rs +++ b/crates/fervid_farmfe/src/lib.rs @@ -81,15 +81,19 @@ impl Plugin for FarmPluginVueFervid { return Ok(None); } + let is_custom_element = param.resolved_path.ends_with(".ce.vue"); + let file_compile_result = fervid::compile( ¶m.content, CompileOptions { filename: std::borrow::Cow::Borrowed(param.resolved_path), id: param.module_id.clone().into(), is_prod: Some(true), + is_custom_element: Some(is_custom_element), + props_destructure: None, ssr: None, gen_default_as: None, - source_map: None + source_map: None, }, ); @@ -113,12 +117,15 @@ impl Plugin for FarmPluginVueFervid { let virtual_module_id = format!("{module_id}{query}"); prepend.push_str(&format!("import '{base_path}{query}'\n")); - virtual_modules.insert(virtual_module_id, PluginLoadHookResult { - content: style.code, - // TODO Determine based on style lang - module_type: ModuleType::Css, - source_map: None, - }); + virtual_modules.insert( + virtual_module_id, + PluginLoadHookResult { + content: style.code, + // TODO Determine based on style lang + module_type: ModuleType::Css, + source_map: None, + }, + ); } } diff --git a/crates/fervid_napi/__tests__/__snapshots__/defineProps.spec.ts.snap b/crates/fervid_napi/__tests__/__snapshots__/defineProps.spec.ts.snap new file mode 100644 index 0000000..bc6f2d6 --- /dev/null +++ b/crates/fervid_napi/__tests__/__snapshots__/defineProps.spec.ts.snap @@ -0,0 +1,821 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`defineProps > basic usage 1`] = ` +"export default { + props: { + foo: String + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + const props = __props; + const bar = 1; + return { + props, + bar + }; + } +}; +" +`; + +exports[`defineProps > custom element retains the props type & default value & production mode 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + foo: { + type: Number, + default: 5.5 + } + }, + __name: "app.ce", + setup (__props) { + interface Props { + foo?: number; + } + const props = __props; + } +}); +" +`; + +exports[`defineProps > custom element retains the props type & production mode 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + foo: { + type: Number + } + }, + __name: "app.ce", + setup (__props) { + const props = __props; + } +}); +" +`; + +exports[`defineProps > defineProps w/ runtime options 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + foo: String + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + const props = __props; + return { + props + }; + } +}); +" +`; + +exports[`defineProps > destructure without enabling reactive destructure 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + foo: { + type: null, + required: true + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + const { foo } = __props; + return { + foo + }; + } +}); +" +`; + +exports[`defineProps > should escape names w/ special symbols 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + "aste*risk": { + type: null, + required: true + }, + "back\\\\slash": { + type: null, + required: true + }, + "semi;colon": { + type: null, + required: true + }, + "curly{bra}ces": { + type: null, + required: true + }, + "amper&sand": { + type: null, + required: true + }, + "round(brack)ets": { + type: null, + required: true + }, + "pl+us": { + type: null, + required: true + }, + "exclamation!mark": { + type: null, + required: true + }, + "single'quote": { + type: null, + required: true + }, + "square[brack]ets": { + type: null, + required: true + }, + "pi|pe": { + type: null, + required: true + }, + "da-sh": { + type: null, + required: true + }, + "til~de": { + type: null, + required: true + }, + dollar$sign: { + type: null, + required: true + }, + "equal=sign": { + type: null, + required: true + }, + 'double"quote': { + type: null, + required: true + }, + "angleets": { + type: null, + required: true + }, + "spa ce": { + type: null, + required: true + }, + "com,ma": { + type: null, + required: true + }, + "do.t": { + type: null, + required: true + }, + "sla/sh": { + type: null, + required: true + }, + "co:lon": { + type: null, + required: true + }, + "at@sign": { + type: null, + required: true + }, + "percentage%sign": { + type: null, + required: true + }, + "ca^ret": { + type: null, + required: true + }, + "question?mark": { + type: null, + required: true + }, + "hash#tag": { + type: null, + required: true + }, + "back\`tick": { + type: null, + required: true + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + } +}); +" +`; + +exports[`defineProps > w/ TS assertion 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: [ + 'foo' + ], + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + } +}); +" +`; + +exports[`defineProps > w/ exported interface 1`] = ` +"export interface Props { + x?: number; +} +import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + x: { + type: Number, + required: false + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + } +}); +" +`; + +exports[`defineProps > w/ exported interface in normal script 1`] = ` +"export interface Props { + x?: number; +} +import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + x: { + type: Number, + required: false + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + } +}); +" +`; + +exports[`defineProps > w/ exported type alias 1`] = ` +"export type Props = { + x?: number; +}; +import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + x: { + type: Number, + required: false + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + } +}); +" +`; + +exports[`defineProps > w/ extends interface 1`] = ` +"interface Foo { + x?: number; +} +import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + z: { + type: Number, + required: true + }, + y: { + type: String, + required: true + }, + x: { + type: Number, + required: false + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + interface Bar extends Foo { + y?: number; + } + interface Props extends Bar { + z: number; + y: string; + } + } +}); +" +`; + +exports[`defineProps > w/ external definition 1`] = ` +"import { propsModel } from './props'; +export default { + props: propsModel, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + const props = __props; + return { + get propsModel () { + return propsModel; + }, + props + }; + } +}; +" +`; + +exports[`defineProps > w/ interface 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + x: { + type: Number, + required: false + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + interface Props { + x?: number; + } + } +}); +" +`; + +exports[`defineProps > w/ leading code 1`] = ` +"import { x } from './x'; +export default { + props: {}, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + const props = __props; + return { + get x () { + return x; + }, + props + }; + } +}; +" +`; + +exports[`defineProps > w/ type 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + unknownIntersection: { + type: Object, + required: true + }, + unknown: { + type: null, + required: true + }, + unknownUnionWithFunction: { + type: Function, + required: true, + skipCheck: true + }, + set: { + type: Set, + required: true + }, + foo: { + type: [ + null, + Function + ], + required: true + }, + intersection: { + type: Object, + required: true + }, + unknownUnionWithBoolean: { + type: Boolean, + required: true, + skipCheck: true + }, + recordRef: { + type: Object, + required: true + }, + literalUnionMixed: { + type: [ + String, + Number, + Boolean + ], + required: true + }, + literal: { + type: String, + required: true + }, + objectRef: { + type: Object, + required: true + }, + nonNull: { + type: String, + required: true + }, + intersection2: { + type: String, + required: true + }, + optional: { + type: null, + required: false + }, + extract: { + type: Number, + required: true + }, + array: { + type: Array, + required: true + }, + tuple: { + type: Array, + required: true + }, + alias: { + type: Array, + required: true + }, + interface: { + type: Object, + required: true + }, + error: { + type: Error, + required: true + }, + uppercase: { + type: String, + required: true + }, + exclude: { + type: [ + Number, + Boolean + ], + required: true + }, + objectOrFn: { + type: [ + Object, + Function + ], + required: true + }, + union: { + type: [ + String, + Number + ], + required: true + }, + arrayRef: { + type: Array, + required: true + }, + boolean: { + type: Boolean, + required: true + }, + unknownUnion: { + type: null, + required: true + }, + literalUnionNumber: { + type: Number, + required: true + }, + object: { + type: Object, + required: true + }, + objectLiteral: { + type: Object, + required: true + }, + fn: { + type: Function, + required: true + }, + string: { + type: String, + required: true + }, + number: { + type: Number, + required: true + }, + dateTime: { + type: Date, + required: true + }, + method: { + type: Function, + required: true + }, + symbol: { + type: Symbol, + required: true + }, + params: { + type: Array, + required: true + }, + literalUnion: { + type: String, + required: true + }, + functionRef: { + type: Function, + required: true + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + interface Test { + } + type Alias = number[]; + } +}); +" +`; + +exports[`defineProps > w/ type alias 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + x: { + type: Number, + required: false + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + type Props = { + x?: number; + }; + } +}); +" +`; + +exports[`defineProps > withDefaults (dynamic) 1`] = ` +"import { defaults } from './foo'; +import { defineComponent as _defineComponent, mergeDefaults as _mergeDefaults } from "vue"; +export default _defineComponent({ + props: _mergeDefaults({ + foo: { + type: String, + required: false + }, + bar: { + type: Number, + required: false + }, + baz: { + type: Boolean, + required: true + } + }, { + ...defaults + }), + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + const props = __props; + return { + props + }; + } +}); +" +`; + +exports[`defineProps > withDefaults (dynamic) w/ production mode 1`] = ` +"import { defaults } from './foo'; +import { defineComponent as _defineComponent, mergeDefaults as _mergeDefaults } from "vue"; +export default _defineComponent({ + props: _mergeDefaults({ + qux: {}, + foo: { + type: Function + }, + bar: { + type: Boolean + }, + baz: { + type: [ + Boolean, + Function + ] + } + }, { + ...defaults + }), + __name: "anonymous", + setup (__props) { + const props = __props; + } +}); +" +`; + +exports[`defineProps > withDefaults (reference) 1`] = ` +"import { defaults } from './foo'; +import { defineComponent as _defineComponent, mergeDefaults as _mergeDefaults } from "vue"; +export default _defineComponent({ + props: _mergeDefaults({ + foo: { + type: String, + required: false + }, + bar: { + type: Number, + required: false + }, + baz: { + type: Boolean, + required: true + } + }, defaults), + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + const props = __props; + return { + props + }; + } +}); +" +`; + +exports[`defineProps > withDefaults (static) + normal script 1`] = ` +"interface Props { + a?: string; +} +import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + a: { + type: String, + required: false, + default: "a" + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + const props = __props; + return { + props + }; + } +}); +" +`; + +exports[`defineProps > withDefaults (static) 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + qux: { + type: Function, + required: false, + default () { + return 1; + } + }, + quux: { + type: Function, + required: false, + default () {} + }, + quuxx: { + type: Promise, + required: false, + async default () { + return await Promise.resolve('hi'); + } + }, + fred: { + type: String, + required: false, + get default () { + return 'fred'; + } + }, + foo: { + type: String, + required: false, + default: 'hi' + }, + bar: { + type: Number, + required: false + }, + baz: { + type: Boolean, + required: true + } + }, + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + const props = __props; + return { + props + }; + } +}); +" +`; + +exports[`defineProps > withDefaults (static) w/ production mode 1`] = ` +"import { defineComponent as _defineComponent } from "vue"; +export default _defineComponent({ + props: { + qux: { + default: 'hi' + }, + foo: {}, + bar: { + type: Boolean + }, + baz: { + type: [ + Boolean, + Function + ], + default: true + } + }, + __name: "anonymous", + setup (__props) { + const props = __props; + } +}); +" +`; + +exports[`defineProps > withDefaults w/ dynamic object method 1`] = ` +"import { defineComponent as _defineComponent, mergeDefaults as _mergeDefaults } from "vue"; +export default _defineComponent({ + props: _mergeDefaults({ + foo: { + type: Function, + required: false + } + }, { + ['fo' + 'o'] () { + return 'foo'; + } + }), + __name: "anonymous", + setup (__props, { expose: __expose }) { + __expose(); + const props = __props; + return { + props + }; + } +}); +" +`; diff --git a/crates/fervid_napi/__tests__/__snapshots__/importUsageCheck.spec.ts.snap b/crates/fervid_napi/__tests__/__snapshots__/importUsageCheck.spec.ts.snap index 3bf30c5..1e1b4ed 100644 --- a/crates/fervid_napi/__tests__/__snapshots__/importUsageCheck.spec.ts.snap +++ b/crates/fervid_napi/__tests__/__snapshots__/importUsageCheck.spec.ts.snap @@ -12,7 +12,7 @@ export default _defineComponent({ _createCommentVNode(" {{ b() }} "), _createTextVNode(" " + _toDisplayString(Baz) + " "), _createVNode(_component_Comp, null, { - "default": _withCtx(()=>[ + default: _withCtx(()=>[ _createTextVNode(_toDisplayString(data), 1) ]) }), diff --git a/crates/fervid_napi/__tests__/defineProps.spec.ts b/crates/fervid_napi/__tests__/defineProps.spec.ts new file mode 100644 index 0000000..4e023ed --- /dev/null +++ b/crates/fervid_napi/__tests__/defineProps.spec.ts @@ -0,0 +1,1080 @@ +import { describe, test, expect } from 'vitest' +import { BindingTypes } from '..' +import { assertCode, compile } from './utils' + +describe('defineProps', () => { + test('basic usage', () => { + const { content, bindings } = compile(` + + `, { outputSetupBindings: true }) + // should generate working code + assertCode(content) + // should analyze bindings + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.LITERAL_CONST, + props: BindingTypes.SETUP_REACTIVE_CONST, + }) + + // should remove defineOptions import and call + expect(content).not.toMatch('defineProps') + // should generate correct setup signature + expect(content).toMatch(`setup (__props, { expose: __expose }) {`) + // should assign user identifier to it + expect(content).toMatch(`const props = __props`) + // should include context options in default export + expect(content).toMatch(`export default { + props: { + foo: String + },`) + }) + + test('w/ external definition', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`export default { + props: propsModel,`) + }) + + // #4764 + test('w/ leading code', () => { + const { content } = compile(` + + `) + // props declaration should be inside setup, not moved along with the import + expect(content).not.toMatch(`const props = __props\nimport`) + assertCode(content) + }) + + test('defineProps w/ runtime options', () => { + const { content } = compile(` + + `) + assertCode(content) + // TODO /*@__PURE__*/ + expect(content).toMatch(`export default _defineComponent({ + props: { + foo: String + }, + __name: "anonymous", + setup (__props, { expose: __expose }) {`) + }) + + test('w/ type', () => { + const { content, bindings } = compile(` + `, { outputSetupBindings: true }) + assertCode(content) + expect(content).toMatch(`string: { + type: String, + required: true + }`) + expect(content).toMatch(`number: { + type: Number, + required: true + }`) + expect(content).toMatch(`boolean: { + type: Boolean, + required: true + }`) + expect(content).toMatch(`object: { + type: Object, + required: true + }`) + expect(content).toMatch(`objectLiteral: { + type: Object, + required: true + }`) + expect(content).toMatch(`fn: { + type: Function, + required: true + }`) + expect(content).toMatch(`functionRef: { + type: Function, + required: true + }`) + expect(content).toMatch(`objectRef: { + type: Object, + required: true + }`) + expect(content).toMatch(`dateTime: { + type: Date, + required: true + }`) + expect(content).toMatch(`array: { + type: Array, + required: true + }`) + expect(content).toMatch(`arrayRef: { + type: Array, + required: true + }`) + expect(content).toMatch(`tuple: { + type: Array, + required: true + }`) + expect(content).toMatch(`set: { + type: Set, + required: true + }`) + expect(content).toMatch(`literal: { + type: String, + required: true + }`) + expect(content).toMatch(`optional: { + type: null, + required: false + }`) + expect(content).toMatch(`recordRef: { + type: Object, + required: true + }`) + expect(content).toMatch(`interface: { + type: Object, + required: true + }`) + expect(content).toMatch(`alias: { + type: Array, + required: true + }`) + expect(content).toMatch(`method: { + type: Function, + required: true + }`) + expect(content).toMatch(`symbol: { + type: Symbol, + required: true + }`) + expect(content).toMatch(`error: { + type: Error, + required: true + }`) + expect(content).toMatch(`objectOrFn: { + type: [ + Object, + Function + ], + required: true + }`) + expect(content).toMatch(`extract: { + type: Number, + required: true + }`) + expect(content).toMatch(`exclude: { + type: [ + Number, + Boolean + ], + required: true + }`) + expect(content).toMatch(`uppercase: { + type: String, + required: true + }`) + expect(content).toMatch(`params: { + type: Array, + required: true + }`) + expect(content).toMatch(`nonNull: { + type: String, + required: true + }`) + expect(content).toMatch(`union: { + type: [ + String, + Number + ], + required: true + }`) + expect(content).toMatch(`literalUnion: { + type: String, + required: true + }`) + expect(content).toMatch(`literalUnionNumber: { + type: Number, + required: true + }`) + expect(content).toMatch(`literalUnionMixed: { + type: [ + String, + Number, + Boolean + ], + required: true + }`) + expect(content).toMatch(`intersection: { + type: Object, + required: true + }`) + expect(content).toMatch(`intersection2: { + type: String, + required: true + }`) + expect(content).toMatch(`foo: { + type: [ + null, + Function + ], + required: true + }`) + expect(content).toMatch(`unknown: { + type: null, + required: true + }`) + // uninon containing unknown type: skip check + expect(content).toMatch(`unknownUnion: { + type: null, + required: true + }`) + // intersection containing unknown type: narrow to the known types + expect(content).toMatch(`unknownIntersection: { + type: Object, + required: true + }`) + expect(content).toMatch(`unknownUnionWithBoolean: { + type: Boolean, + required: true, + skipCheck: true + }`) + expect(content).toMatch(`unknownUnionWithFunction: { + type: Function, + required: true, + skipCheck: true + }`) + expect(bindings).toStrictEqual({ + string: BindingTypes.PROPS, + number: BindingTypes.PROPS, + boolean: BindingTypes.PROPS, + object: BindingTypes.PROPS, + objectLiteral: BindingTypes.PROPS, + fn: BindingTypes.PROPS, + functionRef: BindingTypes.PROPS, + objectRef: BindingTypes.PROPS, + dateTime: BindingTypes.PROPS, + array: BindingTypes.PROPS, + arrayRef: BindingTypes.PROPS, + tuple: BindingTypes.PROPS, + set: BindingTypes.PROPS, + literal: BindingTypes.PROPS, + optional: BindingTypes.PROPS, + recordRef: BindingTypes.PROPS, + interface: BindingTypes.PROPS, + alias: BindingTypes.PROPS, + method: BindingTypes.PROPS, + symbol: BindingTypes.PROPS, + error: BindingTypes.PROPS, + objectOrFn: BindingTypes.PROPS, + extract: BindingTypes.PROPS, + exclude: BindingTypes.PROPS, + union: BindingTypes.PROPS, + literalUnion: BindingTypes.PROPS, + literalUnionNumber: BindingTypes.PROPS, + literalUnionMixed: BindingTypes.PROPS, + intersection: BindingTypes.PROPS, + intersection2: BindingTypes.PROPS, + foo: BindingTypes.PROPS, + uppercase: BindingTypes.PROPS, + params: BindingTypes.PROPS, + nonNull: BindingTypes.PROPS, + unknown: BindingTypes.PROPS, + unknownUnion: BindingTypes.PROPS, + unknownIntersection: BindingTypes.PROPS, + unknownUnionWithBoolean: BindingTypes.PROPS, + unknownUnionWithFunction: BindingTypes.PROPS, + }) + }) + + test('w/ interface', () => { + const { content, bindings } = compile(` + + `, { outputSetupBindings: true }) + assertCode(content) + expect(content).toMatch(`x: { + type: Number, + required: false + }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS, + }) + }) + + test('w/ extends interface', () => { + const { content, bindings } = compile(` + + + `, { outputSetupBindings: true }) + assertCode(content) + expect(content).toMatch(`z: { + type: Number, + required: true + }`) + expect(content).toMatch(`y: { + type: String, + required: true + }`) + expect(content).toMatch(`x: { + type: Number, + required: false + }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS, + y: BindingTypes.PROPS, + z: BindingTypes.PROPS, + }) + }) + + test('w/ exported interface', () => { + const { content, bindings } = compile(` + + `, { outputSetupBindings: true }) + assertCode(content) + expect(content).toMatch(`x: { + type: Number, + required: false + }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS, + }) + }) + + test('w/ exported interface in normal script', () => { + const { content, bindings } = compile(` + + + `, { outputSetupBindings: true }) + assertCode(content) + expect(content).toMatch(`x: { + type: Number, + required: false + }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS, + }) + }) + + test('w/ type alias', () => { + const { content, bindings } = compile(` + + `, { outputSetupBindings: true }) + assertCode(content) + expect(content).toMatch(`x: { + type: Number, + required: false + }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS, + }) + }) + + test('w/ exported type alias', () => { + const { content, bindings } = compile(` + + `, { outputSetupBindings: true }) + assertCode(content) + expect(content).toMatch(`x: { + type: Number, + required: false + }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS, + }) + }) + + test('w/ TS assertion', () => { + const { content, bindings } = compile(` + + `, { outputSetupBindings: true }) + expect(content).toMatch(`props: [ + 'foo' + ]`) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + }) + }) + + test('withDefaults (static)', () => { + const { content, bindings } = compile(` + + `, { outputSetupBindings: true }) + assertCode(content) + expect(content).toMatch(`foo: { + type: String, + required: false, + default: 'hi' + }`) + expect(content).toMatch(`bar: { + type: Number, + required: false + }`) + expect(content).toMatch(`baz: { + type: Boolean, + required: true + }`) + expect(content).toMatch(`qux: { + type: Function, + required: false, + default () { + return 1; + } + }`) + expect(content).toMatch(`quux: { + type: Function, + required: false, + default () {} + }`) + expect(content).toMatch(`quuxx: { + type: Promise, + required: false, + async default () { + return await Promise.resolve('hi'); + } + }`) + expect(content).toMatch(`fred: { + type: String, + required: false, + get default () { + return 'fred'; + } + }`) + expect(content).toMatch(`const props = __props`) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS, + baz: BindingTypes.PROPS, + qux: BindingTypes.PROPS, + quux: BindingTypes.PROPS, + quuxx: BindingTypes.PROPS, + fred: BindingTypes.PROPS, + // Here I assume this is a bug in official compiler because `withDefaults` returns the same + // reactive object as normal `defineProps`, therefore Fervid produces reactive const. + // props: BindingTypes.SETUP_CONST, + props: BindingTypes.SETUP_REACTIVE_CONST, + }) + }) + + test('withDefaults (static) + normal script', () => { + const { content } = compile(` + + + `) + assertCode(content) + }) + + // #7111 + test('withDefaults (static) w/ production mode', () => { + const { content } = compile( + ` + + `, + undefined, + { isProduction: true }, + ) + assertCode(content) + expect(content).toMatch(`const props = __props`) + + // foo has no default value, the Function can be dropped + expect(content).toMatch(`foo: {}`) + expect(content).toMatch(`bar: { + type: Boolean + }`) + expect(content).toMatch(`baz: { + type: [ + Boolean, + Function + ], + default: true + }`) + expect(content).toMatch(`qux: { + default: 'hi' + }`) + }) + + test('withDefaults (dynamic)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(/import {.*mergeDefaults as _mergeDefaults/) + expect(content).toMatch( + `_mergeDefaults({ + foo: { + type: String, + required: false + }, + bar: { + type: Number, + required: false + }, + baz: { + type: Boolean, + required: true + } + }, { + ...defaults + })`.trim()) + }) + + test('withDefaults (reference)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(/import {.*mergeDefaults as _mergeDefaults/) + expect(content).toMatch( + `_mergeDefaults({ + foo: { + type: String, + required: false + }, + bar: { + type: Number, + required: false + }, + baz: { + type: Boolean, + required: true + } + }, defaults)`.trim()) + }) + + // #7111 + test('withDefaults (dynamic) w/ production mode', () => { + const { content } = compile( + ` + + `, + undefined, + { isProduction: true }, + ) + assertCode(content) + expect(content).toMatch(/import {.*mergeDefaults as _mergeDefaults/) + expect(content).toMatch( + `_mergeDefaults({ + qux: {}, + foo: { + type: Function + }, + bar: { + type: Boolean + }, + baz: { + type: [ + Boolean, + Function + ] + } + }, { + ...defaults + })`.trim()) + }) + + test('withDefaults w/ dynamic object method', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(/import {.*mergeDefaults as _mergeDefaults/) + expect(content).toMatch( + `_mergeDefaults({ + foo: { + type: Function, + required: false + } + }, { + ['fo' + 'o'] () { + return 'foo'; + } + })`.trim()) + }) + + // TODO Support hoistStatic + test('runtime inference for Enum', () => { + expect( + compile( + ``, + // { hoistStatic: true }, + ).content, + ).toMatch(`foo: { + type: Number`) + + expect( + compile( + ``, + // { hoistStatic: true }, + ).content, + ).toMatch(`foo: { + type: String`) + + expect( + compile( + ``, + // { hoistStatic: true }, + ).content, + ).toMatch(`foo: { + type: [ + String, + Number + ]`) + + expect( + compile( + ``, + // { hoistStatic: true }, + ).content, + ).toMatch(`foo: { + type: Number`) + }) + + // #8148 + test('should not override local bindings', () => { + const { bindings } = compile(` + + `, { outputSetupBindings: true }) + expect(bindings).toStrictEqual({ + bar: BindingTypes.SETUP_REF, + // computed: BindingTypes.SETUP_CONST, + }) + }) + + // #8289 + test('destructure without enabling reactive destructure', () => { + const { content, bindings } = compile( + ``, + { + propsDestructure: false, + outputSetupBindings: true, + }, + ) + expect(content).toMatch(`const { foo } = __props`) + expect(content).toMatch(`return { + foo + }`) + expect(bindings).toStrictEqual({ + foo: BindingTypes.SETUP_CONST, + }) + assertCode(content) + }) + + test('prohibiting reactive destructure', () => { + const { errors } = compile( + ``, + { + propsDestructure: 'error', + }, + ) + expect(errors.length).toBe(1) + expect(errors[0].message).toMatch('DefinePropsDestructureForbidden') + }) + + describe('errors', () => { + test('w/ both type and non-type args', () => { + const { errors } = compile(``) + + expect(errors.length).toBe(1) + expect(errors[0].message).toMatch('DefinePropsTypeAndNonTypeArguments') + }) + }) + + test('should escape names w/ special symbols', () => { + const { content, bindings } = compile(` + `, { outputSetupBindings: true }) + assertCode(content) + expect(content).toMatch(`"spa ce": { + type: null, + required: true + }`) + expect(content).toMatch(`"exclamation!mark": { + type: null, + required: true + }`) + expect(content).toMatch(`'double"quote': { + type: null, + required: true + }`) + expect(content).toMatch(`"hash#tag": { + type: null, + required: true + }`) + expect(content).toMatch(`dollar$sign: { + type: null, + required: true + }`) + expect(content).toMatch(`"percentage%sign": { + type: null, + required: true + }`) + expect(content).toMatch(`"amper&sand": { + type: null, + required: true + }`) + expect(content).toMatch(`"single'quote": { + type: null, + required: true + }`) + expect(content).toMatch(`"round(brack)ets": { + type: null, + required: true + }`) + expect(content).toMatch(`"aste*risk": { + type: null, + required: true + }`) + expect(content).toMatch(`"pl+us": { + type: null, + required: true + }`) + expect(content).toMatch(`"com,ma": { + type: null, + required: true + }`) + expect(content).toMatch(`"do.t": { + type: null, + required: true + }`) + expect(content).toMatch(`"sla/sh": { + type: null, + required: true + }`) + expect(content).toMatch(`"co:lon": { + type: null, + required: true + }`) + expect(content).toMatch(`"semi;colon": { + type: null, + required: true + }`) + expect(content).toMatch(`"angleets": { + type: null, + required: true + }`) + expect(content).toMatch(`"equal=sign": { + type: null, + required: true + }`) + expect(content).toMatch(`"question?mark": { + type: null, + required: true + }`) + expect(content).toMatch(`"at@sign": { + type: null, + required: true + }`) + expect(content).toMatch(`"square[brack]ets": { + type: null, + required: true + }`) + expect(content).toMatch(`"back\\\\slash": { + type: null, + required: true + }`) + expect(content).toMatch(`"ca^ret": { + type: null, + required: true + }`) + expect(content).toMatch(`"back\`tick": { + type: null, + required: true + }`) + expect(content).toMatch(`"curly{bra}ces": { + type: null, + required: true + }`) + expect(content).toMatch(`"pi|pe": { + type: null, + required: true + }`) + expect(content).toMatch(`"til~de": { + type: null, + required: true + }`) + expect(content).toMatch(`"da-sh": { + type: null, + required: true + }`) + expect(bindings).toStrictEqual({ + 'spa ce': BindingTypes.PROPS, + 'exclamation!mark': BindingTypes.PROPS, + 'double"quote': BindingTypes.PROPS, + 'hash#tag': BindingTypes.PROPS, + dollar$sign: BindingTypes.PROPS, + 'percentage%sign': BindingTypes.PROPS, + 'amper&sand': BindingTypes.PROPS, + "single'quote": BindingTypes.PROPS, + 'round(brack)ets': BindingTypes.PROPS, + 'aste*risk': BindingTypes.PROPS, + 'pl+us': BindingTypes.PROPS, + 'com,ma': BindingTypes.PROPS, + 'do.t': BindingTypes.PROPS, + 'sla/sh': BindingTypes.PROPS, + 'co:lon': BindingTypes.PROPS, + 'semi;colon': BindingTypes.PROPS, + 'angleets': BindingTypes.PROPS, + 'equal=sign': BindingTypes.PROPS, + 'question?mark': BindingTypes.PROPS, + 'at@sign': BindingTypes.PROPS, + 'square[brack]ets': BindingTypes.PROPS, + 'back\\slash': BindingTypes.PROPS, + 'ca^ret': BindingTypes.PROPS, + 'back`tick': BindingTypes.PROPS, + 'curly{bra}ces': BindingTypes.PROPS, + 'pi|pe': BindingTypes.PROPS, + 'til~de': BindingTypes.PROPS, + 'da-sh': BindingTypes.PROPS, + }) + }) + + // #8989 + test('custom element retains the props type & production mode', () => { + const { content } = compile( + ``, + { filename: 'app.ce.vue', isCustomElement: true }, + { isProduction: true, /* customElement: filename => /\.ce\.vue$/.test(filename) */ }, + ) + + expect(content).toMatch(`foo: { + type: Number + }`) + assertCode(content) + }) + + test('custom element retains the props type & default value & production mode', () => { + const { content } = compile( + ``, + { filename: 'app.ce.vue', isCustomElement: true }, + { isProduction: true, /* customElement: filename => /\.ce\.vue$/.test(filename) */ }, + ) + expect(content).toMatch(`foo: { + type: Number, + default: 5.5 + }`) + assertCode(content) + }) +}) diff --git a/crates/fervid_napi/index.d.ts b/crates/fervid_napi/index.d.ts index 0bea89c..cc401e6 100644 --- a/crates/fervid_napi/index.d.ts +++ b/crates/fervid_napi/index.d.ts @@ -55,8 +55,16 @@ export interface FervidCompileOptions { id: string /** Filename is used for automatic component name inference and self-referential imports */ filename: string + /** + * Is the currently compiled file a custom element. + * To give more flexibility, this option only accepts a boolean, allowing to compute the value on the JS side, + * instead of relying on a hacky RegEx/JS function calls from the Fervid side. + */ + isCustomElement?: boolean /** Generate a const instead of default export */ genDefaultAs?: string + /** Enable, disable or error on props destructure */ + propsDestructure?: boolean | 'error' /** Whether setup bindings need to be serialized */ outputSetupBindings?: boolean } diff --git a/crates/fervid_napi/src/lib.rs b/crates/fervid_napi/src/lib.rs index 06401a4..fdd8066 100644 --- a/crates/fervid_napi/src/lib.rs +++ b/crates/fervid_napi/src/lib.rs @@ -6,6 +6,7 @@ static ALLOC: mimalloc_rust::GlobalMiMalloc = mimalloc_rust::GlobalMiMalloc; use std::borrow::Cow; +use fervid_transform::PropsDestructureConfig; use napi::bindgen_prelude::*; use napi_derive::napi; @@ -31,8 +32,8 @@ impl FervidJsCompiler { source: String, options: FervidCompileOptions, ) -> Result { - let compiled = self.compile_impl(&source, &options)?; - Ok(self.convert(env, compiled, &options)) + let compiled = compile_impl(self, &source, &options)?; + Ok(convert(env, compiled, &options)) } #[napi] @@ -49,64 +50,72 @@ impl FervidJsCompiler { }; AsyncTask::with_optional_signal(task, signal) } +} - fn compile_impl( - &self, - source: &str, - options: &FervidCompileOptions, - ) -> Result { - // Normalize options to the ones defined in fervid - let compile_options = CompileOptions { - filename: Cow::Borrowed(&options.filename), - id: Cow::Borrowed(&options.id), - is_prod: self.options.is_production, - ssr: self.options.ssr, - gen_default_as: options - .gen_default_as - .as_ref() - .map(|v| Cow::Borrowed(v.as_str())), - source_map: self.options.source_map, - }; - - compile(source, compile_options).map_err(|e| Error::from_reason(e.to_string())) - } - - fn convert( - &self, - env: Env, - mut result: fervid::CompileResult, - options: &FervidCompileOptions, - ) -> CompileResult { - // Serialize bindings if requested - let setup_bindings = if matches!(options.output_setup_bindings, Some(true)) { - env.create_object() - .map(|mut obj| { - for binding in result.setup_bindings.drain(..) { - let _ = obj.set(binding.0.as_str(), BindingTypes::from(binding.1)); - } - obj - }) - .ok() - } else { - None - }; +fn compile_impl( + compiler: &FervidJsCompiler, + source: &str, + options: &FervidCompileOptions, +) -> Result { + let props_destructure = match options.props_destructure { + Some(Either::A(true)) => Some(PropsDestructureConfig::True), + Some(Either::A(false)) => Some(PropsDestructureConfig::False), + Some(Either::B(ref s)) if s == "error" => Some(PropsDestructureConfig::Error), + _ => None, + }; + + // Normalize options to the ones defined in fervid + let compile_options = CompileOptions { + filename: Cow::Borrowed(&options.filename), + id: Cow::Borrowed(&options.id), + is_prod: compiler.options.is_production, + is_custom_element: options.is_custom_element, + props_destructure, + ssr: compiler.options.ssr, + gen_default_as: options + .gen_default_as + .as_ref() + .map(|v| Cow::Borrowed(v.as_str())), + source_map: compiler.options.source_map, + }; + + compile(source, compile_options).map_err(|e| Error::from_reason(e.to_string())) +} - CompileResult { - code: result.code, - source_map: result.source_map, - custom_blocks: result - .other_assets - .into_iter() - .map(|asset| asset.into()) - .collect(), - errors: result.errors.into_iter().map(|e| e.into()).collect(), - styles: result - .styles - .into_iter() - .map(|style| style.into()) - .collect(), - setup_bindings, - } +fn convert( + env: Env, + mut result: fervid::CompileResult, + options: &FervidCompileOptions, +) -> CompileResult { + // Serialize bindings if requested + let setup_bindings = if matches!(options.output_setup_bindings, Some(true)) { + env.create_object() + .map(|mut obj| { + for binding in result.setup_bindings.drain(..) { + let _ = obj.set(binding.0.as_str(), BindingTypes::from(binding.1)); + } + obj + }) + .ok() + } else { + None + }; + + CompileResult { + code: result.code, + source_map: result.source_map, + custom_blocks: result + .other_assets + .into_iter() + .map(|asset| asset.into()) + .collect(), + errors: result.errors.into_iter().map(|e| e.into()).collect(), + styles: result + .styles + .into_iter() + .map(|style| style.into()) + .collect(), + setup_bindings, } } @@ -122,10 +131,10 @@ impl Task for CompileTask { type Output = fervid::CompileResult; fn compute(&mut self) -> napi::Result { - self.compiler.compile_impl(&self.input, &self.options) + compile_impl(&self.compiler, &self.input, &self.options) } fn resolve(&mut self, env: Env, result: Self::Output) -> napi::Result { - Ok(self.compiler.convert(env, result, &self.options)) + Ok(convert(env, result, &self.options)) } } diff --git a/crates/fervid_napi/src/structs.rs b/crates/fervid_napi/src/structs.rs index 79f804b..f66e05a 100644 --- a/crates/fervid_napi/src/structs.rs +++ b/crates/fervid_napi/src/structs.rs @@ -1,4 +1,4 @@ -use napi::JsObject; +use napi::{Either, JsObject}; use napi_derive::napi; use swc_core::common::Spanned; @@ -76,10 +76,22 @@ pub struct FervidJsCompilerOptionsStyle { pub struct FervidCompileOptions { /// Scope ID for prefixing injected CSS variables pub id: String, + /// Filename is used for automatic component name inference and self-referential imports pub filename: String, + + /// Is the currently compiled file a custom element. + /// To give more flexibility, this option only accepts a boolean, allowing to compute the value on the JS side, + /// instead of relying on a hacky RegEx/JS function calls from the Fervid side. + pub is_custom_element: Option, + /// Generate a const instead of default export pub gen_default_as: Option, + + /// Enable, disable or error on props destructure + #[napi(ts_type = "boolean | 'error'")] + pub props_destructure: Option>, + /// Whether setup bindings need to be serialized pub output_setup_bindings: Option, } diff --git a/crates/fervid_transform/src/error.rs b/crates/fervid_transform/src/error.rs index aadf6e1..12e9bcf 100644 --- a/crates/fervid_transform/src/error.rs +++ b/crates/fervid_transform/src/error.rs @@ -35,6 +35,8 @@ pub enum ScriptErrorKind { DefineOptionsExpose, /// "`defineOptions` cannot be used to declare slots. Use defineSlots() instead." DefineOptionsSlots, + /// `Props destructure is explicitly prohibited via config.` + DefinePropsDestructureForbidden, /// "`defineSlots` cannot accept arguments" DefineSlotsArguments, /// Duplicate `defineEmits` call diff --git a/crates/fervid_transform/src/lib.rs b/crates/fervid_transform/src/lib.rs index fd2c8c9..3f6317d 100644 --- a/crates/fervid_transform/src/lib.rs +++ b/crates/fervid_transform/src/lib.rs @@ -103,7 +103,8 @@ impl TransformSfcContext { TransformSfcContext { filename: options.filename.to_string(), - is_ce: false, // todo + is_ce: options.is_ce, + props_destructure: options.props_destructure, bindings_helper, deps: Default::default(), scopes: vec![], diff --git a/crates/fervid_transform/src/script/resolve_type.rs b/crates/fervid_transform/src/script/resolve_type.rs index 4359122..36fca61 100644 --- a/crates/fervid_transform/src/script/resolve_type.rs +++ b/crates/fervid_transform/src/script/resolve_type.rs @@ -4149,6 +4149,8 @@ mod tests { &sfc_descriptor, &crate::TransformSfcOptions { is_prod: true, + is_ce: false, + props_destructure: crate::PropsDestructureConfig::default(), scope_id: "test", filename: "./Test.vue", }, diff --git a/crates/fervid_transform/src/script/setup.rs b/crates/fervid_transform/src/script/setup.rs index 060b62e..bd3eae1 100644 --- a/crates/fervid_transform/src/script/setup.rs +++ b/crates/fervid_transform/src/script/setup.rs @@ -28,6 +28,7 @@ mod define_emits; mod define_model; mod define_options; mod define_props; +mod define_props_destructure; mod define_slots; mod macros; mod utils; @@ -101,6 +102,7 @@ pub fn transform_and_record_script_setup( &mut sfc_object_helper, false, false, + false, None, errors, ); @@ -241,6 +243,7 @@ fn transform_decl_stmt( init_expr, sfc_object_helper, true, + is_const, is_ident, Some(&mut collected_bindings), errors, diff --git a/crates/fervid_transform/src/script/setup/define_options.rs b/crates/fervid_transform/src/script/setup/define_options.rs index 8b59c19..5835307 100644 --- a/crates/fervid_transform/src/script/setup/define_options.rs +++ b/crates/fervid_transform/src/script/setup/define_options.rs @@ -4,8 +4,7 @@ use swc_core::{ }; use crate::{ - error::{ScriptError, ScriptErrorKind, TransformError}, - SfcExportedObjectHelper, + error::{ScriptError, ScriptErrorKind, TransformError}, script::setup::utils::unwrap_ts_node_expr, SfcExportedObjectHelper }; use super::macros::TransformMacroResult; @@ -121,14 +120,3 @@ pub fn process_define_options( valid_macro!(None) } - -fn unwrap_ts_node_expr(expr: &Expr) -> &Expr { - match expr { - Expr::TsConstAssertion(ts_const_assertion) => unwrap_ts_node_expr(&ts_const_assertion.expr), - Expr::TsNonNull(ts_non_null_expr) => unwrap_ts_node_expr(&ts_non_null_expr.expr), - Expr::TsAs(ts_as_expr) => unwrap_ts_node_expr(&ts_as_expr.expr), - Expr::TsInstantiation(ts_instantiation) => unwrap_ts_node_expr(&ts_instantiation.expr), - Expr::TsSatisfies(ts_satisfies_expr) => unwrap_ts_node_expr(&ts_satisfies_expr.expr), - _ => expr - } -} diff --git a/crates/fervid_transform/src/script/setup/define_props.rs b/crates/fervid_transform/src/script/setup/define_props.rs index db9f108..126b9d1 100644 --- a/crates/fervid_transform/src/script/setup/define_props.rs +++ b/crates/fervid_transform/src/script/setup/define_props.rs @@ -1,4 +1,4 @@ -use fervid_core::{fervid_atom, BindingTypes, FervidAtom, IntoIdent, VueImports}; +use fervid_core::{atom_to_propname, fervid_atom, BindingTypes, FervidAtom, IntoIdent, VueImports}; use flagset::FlagSet; use swc_core::{ common::{Span, Spanned, DUMMY_SP}, @@ -22,7 +22,7 @@ use crate::{ SetupBinding, SfcExportedObjectHelper, }; -use super::macros::TransformMacroResult; +use super::{define_props_destructure::process_props_destructure, macros::TransformMacroResult}; #[derive(Default)] struct DefineProps { @@ -39,18 +39,35 @@ pub fn process_define_props( ctx: &mut TypeResolveContext, call_expr: &CallExpr, is_var_decl: bool, + is_const: bool, + is_ident: bool, + var_bindings: Option<&mut Vec>, sfc_object_helper: &mut SfcExportedObjectHelper, + errors: &mut Vec, ) -> TransformMacroResult { let mut define_props = DefineProps::default(); extract_from_define_props(call_expr, &mut define_props); - process_define_props_impl(ctx, define_props, is_var_decl, sfc_object_helper) + process_define_props_impl( + ctx, + define_props, + is_var_decl, + is_const, + is_ident, + var_bindings, + sfc_object_helper, + errors, + ) } pub fn process_with_defaults( ctx: &mut TypeResolveContext, with_defaults_call: &CallExpr, is_var_decl: bool, + is_const: bool, + is_ident: bool, + var_bindings: Option<&mut Vec>, sfc_object_helper: &mut SfcExportedObjectHelper, + errors: &mut Vec, ) -> TransformMacroResult { macro_rules! bail_no_define_props { () => { @@ -86,7 +103,16 @@ pub fn process_with_defaults( define_props.defaults = with_defaults_call.args.get(1).map(|v| v.expr.to_owned()); // Process - process_define_props_impl(ctx, define_props, is_var_decl, sfc_object_helper) + process_define_props_impl( + ctx, + define_props, + is_var_decl, + is_const, + is_ident, + var_bindings, + sfc_object_helper, + errors, + ) // TODO Implement a more generic `process_define_props_impl` function // which will return values to be assembled by `process_define_props` and `process_with_defaults`. @@ -113,7 +139,11 @@ fn process_define_props_impl( ctx: &mut TypeResolveContext, define_props: DefineProps, is_var_decl: bool, + is_const: bool, + is_ident: bool, + var_bindings: Option<&mut Vec>, sfc_object_helper: &mut SfcExportedObjectHelper, + errors: &mut Vec, ) -> TransformMacroResult { // Check duplicate if sfc_object_helper.props.is_some() { @@ -181,6 +211,25 @@ fn process_define_props_impl( if is_var_decl { sfc_object_helper.is_setup_props_referenced = true; + // TODO Refactor `is_var_decl`, `is_const`, `is_ident`, `var_bindings` into a single helper + // TODO Condition here should look at `ObjectPat` instead + if define_props.defaults.is_none() && !is_ident { + process_props_destructure(ctx, errors); + } + + // Binding type of the prop variable itself + if let Some(var_bindings) = var_bindings { + if is_ident && var_bindings.len() == 1 { + let binding = &mut var_bindings[0]; + binding.1 = BindingTypes::SetupReactiveConst; + } else if is_const { + // `defineProps` with a destructured const variable is `SetupConst` + var_bindings + .iter_mut() + .for_each(|v| v.1 = BindingTypes::SetupConst); + } + } + TransformMacroResult::ValidMacro(Some(Box::new(Expr::Ident( PROPS_HELPER .to_owned() @@ -291,7 +340,7 @@ fn resolve_runtime_props_from_type( // Skip check for result containing unknown types let mut skip_check = false; if types.contains(Types::Unknown) { - if types.contains(Types::Boolean | Types::Function) { + if types.contains(Types::Boolean) || types.contains(Types::Function) { types -= Types::Unknown; skip_check = true; } else { @@ -352,7 +401,11 @@ fn get_runtime_prop_from_type( PropName::Ident(ref ident) => &key == &ident.sym, PropName::Str(ref s) => &key == &s.value, PropName::Num(ref n) => &key == &n.value.to_string(), - PropName::Computed(_) => false, + PropName::Computed(ref c) => match c.expr.as_ref() { + Expr::Lit(Lit::Str(s)) => &key == &s.value, + Expr::Lit(Lit::Num(n)) => &key == &n.value.to_string(), + _ => false, + }, PropName::BigInt(_) => false, } }; @@ -419,8 +472,6 @@ fn get_runtime_prop_from_type( } } - // Difference from official compiler: no key escaping happens, as we rely on SWC - // For return value let mut prop_object_fields: Vec = Vec::with_capacity(4); @@ -436,14 +487,13 @@ fn get_runtime_prop_from_type( }; } + // TODO Better span is probably possible (preserve prop name span) + let key = atom_to_propname(key.to_owned(), DUMMY_SP); + macro_rules! return_value { ($prop_object_fields: ident) => { PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - // TODO Better span is probably possible (preserve prop name span) - key: PropName::Ident(IdentName { - span: DUMMY_SP, - sym: key.to_owned(), - }), + key, value: Box::new(Expr::Object(ObjectLit { // TODO Better span is probably possible (preserve prop defaults span?) span: DUMMY_SP, @@ -542,7 +592,10 @@ fn has_static_with_defaults(defaults: Option<&Expr>) -> bool { Prop::Assign(_) => return true, }; - !matches!(key, PropName::Computed(_)) + match key { + PropName::Computed(computed_prop_name) => computed_prop_name.expr.is_lit(), + _ => true, + } }) } diff --git a/crates/fervid_transform/src/script/setup/define_props_destructure.rs b/crates/fervid_transform/src/script/setup/define_props_destructure.rs new file mode 100644 index 0000000..5678615 --- /dev/null +++ b/crates/fervid_transform/src/script/setup/define_props_destructure.rs @@ -0,0 +1,19 @@ +use swc_core::common::DUMMY_SP; + +use crate::{error::{ScriptError, TransformError}, script::resolve_type::TypeResolveContext, PropsDestructureConfig}; + +pub fn process_props_destructure( + ctx: &mut TypeResolveContext, + errors: &mut Vec +) { + match ctx.props_destructure { + PropsDestructureConfig::False => return, + PropsDestructureConfig::True => {}, + PropsDestructureConfig::Error => { + errors.push(TransformError::ScriptError(ScriptError { + span: DUMMY_SP, // TODO + kind: crate::error::ScriptErrorKind::DefinePropsDestructureForbidden, + })); + }, + } +} \ No newline at end of file diff --git a/crates/fervid_transform/src/script/setup/macros.rs b/crates/fervid_transform/src/script/setup/macros.rs index 963165b..850c95b 100644 --- a/crates/fervid_transform/src/script/setup/macros.rs +++ b/crates/fervid_transform/src/script/setup/macros.rs @@ -18,6 +18,7 @@ use crate::{ define_options::process_define_options, define_props::{process_define_props, process_with_defaults}, define_slots::process_define_slots, + utils::unwrap_ts_node_expr, }, }, structs::SfcExportedObjectHelper, @@ -42,6 +43,7 @@ pub fn transform_script_setup_macro_expr( expr: &Expr, sfc_object_helper: &mut SfcExportedObjectHelper, is_var_decl: bool, + is_const: bool, is_ident: bool, var_bindings: Option<&mut Vec>, errors: &mut Vec, @@ -68,6 +70,9 @@ pub fn transform_script_setup_macro_expr( }; } + // Strip TS to get to the actual expression + let expr = unwrap_ts_node_expr(expr); + // Script setup macros are calls let Expr::Call(ref call_expr) = *expr else { bail!(); @@ -90,9 +95,27 @@ pub fn transform_script_setup_macro_expr( let sym = &callee_ident.sym; let span = call_expr.span; if DEFINE_PROPS.eq(sym) { - process_define_props(ctx, call_expr, is_var_decl, sfc_object_helper) + process_define_props( + ctx, + call_expr, + is_var_decl, + is_const, + is_ident, + var_bindings, + sfc_object_helper, + errors, + ) } else if WITH_DEFAULTS.eq(sym) { - process_with_defaults(ctx, call_expr, is_var_decl, sfc_object_helper) + process_with_defaults( + ctx, + call_expr, + is_var_decl, + is_const, + is_ident, + var_bindings, + sfc_object_helper, + errors, + ) } else if DEFINE_EMITS.eq(sym) { process_define_emits( ctx, diff --git a/crates/fervid_transform/src/script/setup/utils.rs b/crates/fervid_transform/src/script/setup/utils.rs index 37b4e49..5cbc2ec 100644 --- a/crates/fervid_transform/src/script/setup/utils.rs +++ b/crates/fervid_transform/src/script/setup/utils.rs @@ -33,3 +33,14 @@ pub fn to_runtime_type_string(types: TypesSet) -> Box { elems: array_elems, })) } + +pub fn unwrap_ts_node_expr(expr: &Expr) -> &Expr { + match expr { + Expr::TsConstAssertion(ts_const_assertion) => unwrap_ts_node_expr(&ts_const_assertion.expr), + Expr::TsNonNull(ts_non_null_expr) => unwrap_ts_node_expr(&ts_non_null_expr.expr), + Expr::TsAs(ts_as_expr) => unwrap_ts_node_expr(&ts_as_expr.expr), + Expr::TsInstantiation(ts_instantiation) => unwrap_ts_node_expr(&ts_instantiation.expr), + Expr::TsSatisfies(ts_satisfies_expr) => unwrap_ts_node_expr(&ts_satisfies_expr.expr), + _ => expr + } +} diff --git a/crates/fervid_transform/src/structs.rs b/crates/fervid_transform/src/structs.rs index bebc14f..424f8bb 100644 --- a/crates/fervid_transform/src/structs.rs +++ b/crates/fervid_transform/src/structs.rs @@ -16,6 +16,8 @@ use swc_core::ecma::ast::{ pub struct TransformSfcContext { pub filename: String, // pub is_prod: bool, // This is a part of BindingsHelper + /// Enable/disable the props destructure, or error when usage is encountered + pub props_destructure: PropsDestructureConfig, /// For Custom Elements pub is_ce: bool, pub bindings_helper: BindingsHelper, @@ -23,6 +25,14 @@ pub struct TransformSfcContext { pub(crate) scopes: Vec, } +#[derive(Debug, Clone, Copy, Default)] +pub enum PropsDestructureConfig { + #[default] + False, + True, + Error +} + /// A helper which encompasses all the logic related to bindings, /// such as their types, which of them were used, what components and directives /// were seen in the template, etc. @@ -181,6 +191,8 @@ pub struct TransformScriptsResult { pub struct TransformSfcOptions<'s> { pub is_prod: bool, + pub is_ce: bool, + pub props_destructure: PropsDestructureConfig, pub scope_id: &'s str, pub filename: &'s str, } @@ -210,6 +222,7 @@ impl TransformSfcContext { filename: filename.to_owned(), bindings_helper: BindingsHelper::default(), is_ce: false, + props_destructure: PropsDestructureConfig::default(), deps: HashSet::default(), scopes: vec![], } diff --git a/crates/fervid_wasm/src/lib.rs b/crates/fervid_wasm/src/lib.rs index c68bbb2..cdfd5a3 100644 --- a/crates/fervid_wasm/src/lib.rs +++ b/crates/fervid_wasm/src/lib.rs @@ -33,6 +33,8 @@ pub fn compile_sync(source: &str, is_prod: Option) -> Result