fervid_codegen/directives/
mod.rs

1use fervid_core::{CustomDirectiveBinding, FervidAtom, StrOrExpr, VueDirectives, VueImports};
2use swc_core::{
3    common::{Span, DUMMY_SP},
4    ecma::ast::{
5        ArrayLit, BindingIdent, Bool, CallExpr, Callee, Expr, ExprOrSpread, Ident, KeyValueProp,
6        Lit, Number, ObjectLit, Pat, Prop, PropOrSpread, Str, UnaryExpr, UnaryOp, VarDeclarator,
7    },
8};
9
10use crate::{utils::str_to_propname, CodegenContext};
11
12mod v_for;
13mod v_html;
14mod v_memo;
15mod v_model;
16mod v_once;
17mod v_text;
18
19impl CodegenContext {
20    pub fn generate_directives_to_array(
21        &mut self,
22        directives: &VueDirectives,
23        out: &mut Vec<Option<ExprOrSpread>>,
24    ) {
25        // Check for work and possibly pre-allocate
26        macro_rules! has {
27            ($key: ident) => {
28                directives.$key.is_some() as usize
29            };
30        }
31        let total_work = directives.custom.len() + has!(v_show) + has!(v_memo);
32        if total_work == 0 {
33            return;
34        }
35
36        // Pre-allocate
37        out.reserve(total_work);
38
39        // v-show
40        if let Some(ref v_show) = directives.v_show {
41            let span = DUMMY_SP; // TODO Span
42            let v_show_identifier = Expr::Ident(Ident {
43                span,
44                sym: self.get_and_add_import_ident(VueImports::VShow),
45                optional: false,
46            });
47
48            out.push(Some(ExprOrSpread {
49                spread: None,
50                expr: Box::new(self.generate_directive_from_parts(
51                    v_show_identifier,
52                    Some(v_show),
53                    None,
54                    &[],
55                    span,
56                )),
57            }))
58        }
59
60        // Generate custom directives last
61        for custom_directive in directives.custom.iter() {
62            let span = DUMMY_SP; // TODO Span
63            let directive_ident = self.get_custom_directive_ident(&custom_directive.name, span);
64
65            out.push(Some(ExprOrSpread {
66                spread: None,
67                expr: Box::new(self.generate_directive_from_parts(
68                    directive_ident,
69                    custom_directive.value.as_deref(),
70                    custom_directive.argument.as_ref(),
71                    &custom_directive.modifiers,
72                    span,
73                )),
74            }));
75        }
76    }
77
78    /// Generates `withDirectives(/* render code */, [/* directives array */])`
79    pub fn maybe_generate_with_directives(
80        &mut self,
81        expr: Expr,
82        directives_arr: Vec<Option<ExprOrSpread>>,
83    ) -> Expr {
84        if directives_arr.is_empty() {
85            return expr;
86        }
87
88        Expr::Call(CallExpr {
89            span: DUMMY_SP, // TODO Span
90            callee: Callee::Expr(Box::new(Expr::Ident(Ident {
91                span: DUMMY_SP,
92                sym: self.get_and_add_import_ident(VueImports::WithDirectives),
93                optional: false,
94            }))),
95            args: vec![
96                ExprOrSpread {
97                    spread: None,
98                    expr: Box::new(expr),
99                },
100                ExprOrSpread {
101                    spread: None,
102                    expr: Box::new(Expr::Array(ArrayLit {
103                        span: DUMMY_SP,
104                        elems: directives_arr,
105                    })),
106                },
107            ],
108            type_args: None,
109        })
110    }
111
112    /// Generates a generalized directive in form
113    /// `[
114    ///   directive_ident,
115    ///   directive_expression?,
116    ///   directive_arg?,
117    ///   { modifier1: true, modifier2: true }?
118    /// ]`.
119    ///
120    /// This typically applies to custom directives, `v-show` and element `v-model`
121    pub fn generate_directive_from_parts(
122        &mut self,
123        identifier: Expr,
124        value: Option<&Expr>,
125        argument: Option<&StrOrExpr>,
126        modifiers: &[FervidAtom],
127        span: Span,
128    ) -> Expr {
129        let has_argument = argument.is_some();
130        let has_modifiers = modifiers.len() > 0;
131
132        // Array and size hint
133        let directive_arr_len_hint = if has_modifiers {
134            4
135        } else if has_argument {
136            3
137        } else if value.is_some() {
138            2
139        } else {
140            1
141        };
142        let mut directive_arr = ArrayLit {
143            span,
144            elems: Vec::with_capacity(directive_arr_len_hint),
145        };
146
147        // Directive name
148        // let directive_ident = self.get_custom_directive_ident(custom_directive.name, DUMMY_SP);
149        directive_arr.elems.push(Some(ExprOrSpread {
150            spread: None,
151            expr: Box::new(identifier),
152        }));
153
154        // Tries to early exit if we reached the desired array length
155        macro_rules! early_exit {
156            ($desired: expr) => {
157                if directive_arr_len_hint == $desired {
158                    return Expr::Array(directive_arr);
159                }
160            };
161        }
162
163        early_exit!(1);
164
165        // Write the value or `void 0`
166        directive_arr.elems.push(Some(ExprOrSpread {
167            spread: None,
168            expr: if let Some(directive_value) = value {
169                Box::new(directive_value.to_owned())
170            } else {
171                Box::new(void0())
172            },
173        }));
174
175        early_exit!(2);
176
177        // Write the argument or `void 0`
178        let directive_arg_expr = match argument {
179            Some(StrOrExpr::Str(s)) => Box::new(Expr::Lit(Lit::Str(Str {
180                span: DUMMY_SP,
181                value: s.to_owned(),
182                raw: None,
183            }))),
184            Some(StrOrExpr::Expr(expr)) => expr.to_owned(),
185            None => Box::new(void0()),
186        };
187        directive_arr.elems.push(Some(ExprOrSpread {
188            spread: None,
189            expr: directive_arg_expr,
190        }));
191
192        early_exit!(3);
193
194        // Write the modifiers object in form `{ mod1: true, mod2: true }`
195        let mut modifiers_obj = ObjectLit {
196            span: DUMMY_SP,
197            props: Vec::with_capacity(modifiers.len()),
198        };
199        for modifier in modifiers.iter() {
200            modifiers_obj
201                .props
202                .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
203                    key: str_to_propname(&modifier, DUMMY_SP),
204                    value: Box::new(Expr::Lit(Lit::Bool(Bool {
205                        span: DUMMY_SP,
206                        value: true,
207                    }))),
208                }))))
209        }
210        directive_arr.elems.push(Some(ExprOrSpread {
211            spread: None,
212            expr: Box::new(Expr::Object(modifiers_obj)),
213        }));
214
215        Expr::Array(directive_arr)
216    }
217
218    fn get_custom_directive_ident(&mut self, directive_name: &FervidAtom, span: Span) -> Expr {
219        // Check directive existence and early exit
220        let existing_directive_binding = self.bindings_helper.custom_directives.get(directive_name);
221        match existing_directive_binding {
222            Some(CustomDirectiveBinding::Resolved(directive_binding)) => {
223                return (**directive_binding).to_owned()
224            }
225            Some(CustomDirectiveBinding::RuntimeResolved(directive_ident)) => {
226                return Expr::Ident((**directive_ident).to_owned())
227            }
228            _ => {}
229        }
230
231        // _directive_ prefix plus directive name
232        let mut directive_ident_raw = directive_name.replace('-', "_");
233        directive_ident_raw.insert_str(0, "_directive_");
234        let directive_ident_atom = FervidAtom::from(directive_ident_raw);
235
236        // Directive will be resolved during runtime, this provides a variable name,
237        // e.g. `const _directive_custom = resolveDirective('custom')`
238        // and later `withDirectives(/*component*/, [[_directive_custom]])`
239        let resolve_identifier = Ident {
240            span,
241            sym: directive_ident_atom,
242            optional: false,
243        };
244
245        // Add as a runtime resolution
246        self.bindings_helper.custom_directives.insert(
247            directive_name.to_owned(),
248            CustomDirectiveBinding::RuntimeResolved(Box::new(resolve_identifier.to_owned())),
249        );
250
251        Expr::Ident(resolve_identifier)
252    }
253
254    pub fn generate_directive_resolves(&mut self) -> Vec<VarDeclarator> {
255        let mut result = Vec::new();
256
257        if self.bindings_helper.custom_directives.len() == 0 {
258            return result;
259        }
260
261        let resolve_directive_ident = self.get_and_add_import_ident(VueImports::ResolveDirective);
262
263        // We need sorted entries for stable output.
264        // Entries are sorted by directive name (first element of tuple in hashmap entry)
265        let mut sorted_directives: Vec<(&FervidAtom, &Ident)> = self
266            .bindings_helper
267            .custom_directives
268            .iter()
269            .filter_map(
270                |(directive_name, directive_resolution)| match directive_resolution {
271                    CustomDirectiveBinding::RuntimeResolved(ident) => {
272                        Some((directive_name, ident.as_ref()))
273                    }
274                    _ => None,
275                },
276            )
277            .collect();
278
279        sorted_directives.sort_by(|a, b| a.0.cmp(b.0));
280
281        // Key is a component as used in template, value is the assigned Js identifier
282        for (directive_name, directive_identifier) in sorted_directives.iter() {
283            // _directive_ident_name = resolveDirective("directive-template-name")
284            result.push(VarDeclarator {
285                span: DUMMY_SP,
286                name: Pat::Ident(BindingIdent {
287                    id: (*directive_identifier).to_owned(),
288                    type_ann: None,
289                }),
290                init: Some(Box::new(Expr::Call(CallExpr {
291                    span: DUMMY_SP,
292                    callee: Callee::Expr(Box::new(Expr::Ident(Ident {
293                        span: DUMMY_SP,
294                        sym: resolve_directive_ident.to_owned(),
295                        optional: false,
296                    }))),
297                    args: vec![ExprOrSpread {
298                        spread: None,
299                        expr: Box::new(Expr::Lit(Lit::Str(Str {
300                            span: DUMMY_SP,
301                            value: (**directive_name).to_owned(),
302                            raw: None,
303                        }))),
304                    }],
305                    type_args: None,
306                }))),
307                definite: false,
308            });
309        }
310
311        result
312    }
313}
314
315/// Generates `void 0` expression
316fn void0() -> Expr {
317    Expr::Unary(UnaryExpr {
318        span: DUMMY_SP,
319        op: UnaryOp::Void,
320        arg: Box::new(Expr::Lit(Lit::Num(Number {
321            raw: None,
322            span: DUMMY_SP,
323            value: 0.0,
324        }))),
325    })
326}