swc_ecma_transforms_react/refresh/
mod.rs

1use rustc_hash::FxHashSet;
2use swc_common::{
3    comments::Comments, sync::Lrc, util::take::Take, BytePos, Mark, SourceMap, SourceMapper, Span,
4    Spanned, SyntaxContext, DUMMY_SP,
5};
6use swc_ecma_ast::*;
7use swc_ecma_utils::{private_ident, quote_ident, quote_str, ExprFactory};
8use swc_ecma_visit::{visit_mut_pass, Visit, VisitMut, VisitMutWith};
9
10use self::{
11    hook::HookRegister,
12    util::{collect_ident_in_jsx, is_body_arrow_fn, is_import_or_require, make_assign_stmt},
13};
14
15pub mod options;
16use options::RefreshOptions;
17mod hook;
18mod util;
19
20#[cfg(test)]
21mod tests;
22
23struct Hoc {
24    insert: bool,
25    reg: Vec<(Ident, Id)>,
26    hook: Option<HocHook>,
27}
28struct HocHook {
29    callee: Callee,
30    rest_arg: Vec<ExprOrSpread>,
31}
32enum Persist {
33    Hoc(Hoc),
34    Component(Ident),
35    None,
36}
37fn get_persistent_id(ident: &Ident) -> Persist {
38    if ident.sym.starts_with(|c: char| c.is_ascii_uppercase()) {
39        if cfg!(debug_assertions) && ident.ctxt == SyntaxContext::empty() {
40            panic!("`{}` should be resolved", ident)
41        }
42        Persist::Component(ident.clone())
43    } else {
44        Persist::None
45    }
46}
47
48/// `react-refresh/babel`
49/// https://github.com/facebook/react/blob/main/packages/react-refresh/src/ReactFreshBabelPlugin.js
50pub fn refresh<C: Comments>(
51    dev: bool,
52    options: Option<RefreshOptions>,
53    cm: Lrc<SourceMap>,
54    comments: Option<C>,
55    global_mark: Mark,
56) -> impl Pass {
57    visit_mut_pass(Refresh {
58        enable: dev && options.is_some(),
59        cm,
60        comments,
61        should_reset: false,
62        options: options.unwrap_or_default(),
63        global_mark,
64    })
65}
66
67struct Refresh<C: Comments> {
68    enable: bool,
69    options: RefreshOptions,
70    cm: Lrc<SourceMap>,
71    should_reset: bool,
72    comments: Option<C>,
73    global_mark: Mark,
74}
75
76impl<C: Comments> Refresh<C> {
77    fn get_persistent_id_from_var_decl(
78        &self,
79        var_decl: &mut VarDecl,
80        used_in_jsx: &FxHashSet<Id>,
81        hook_reg: &mut HookRegister,
82    ) -> Persist {
83        // We only handle the case when a single variable is declared
84        if let [VarDeclarator {
85            name: Pat::Ident(binding),
86            init: Some(init_expr),
87            ..
88        }] = var_decl.decls.as_mut_slice()
89        {
90            if used_in_jsx.contains(&binding.to_id()) && !is_import_or_require(init_expr) {
91                match init_expr.as_ref() {
92                    // TaggedTpl is for something like styled.div`...`
93                    Expr::Arrow(_) | Expr::Fn(_) | Expr::TaggedTpl(_) | Expr::Call(_) => {
94                        return Persist::Component(Ident::from(&*binding))
95                    }
96                    _ => (),
97                }
98            }
99
100            if let Persist::Component(persistent_id) = get_persistent_id(&Ident::from(&*binding)) {
101                return match init_expr.as_mut() {
102                    Expr::Fn(_) => Persist::Component(persistent_id),
103                    Expr::Arrow(ArrowExpr { body, .. }) => {
104                        // Ignore complex function expressions like
105                        // let Foo = () => () => {}
106                        if is_body_arrow_fn(body) {
107                            Persist::None
108                        } else {
109                            Persist::Component(persistent_id)
110                        }
111                    }
112                    // Maybe a HOC.
113                    Expr::Call(call_expr) => {
114                        let res = self.get_persistent_id_from_possible_hoc(
115                            call_expr,
116                            vec![(private_ident!("_c"), persistent_id.to_id())],
117                            hook_reg,
118                        );
119                        if let Persist::Hoc(Hoc {
120                            insert,
121                            reg,
122                            hook: Some(hook),
123                        }) = res
124                        {
125                            make_hook_reg(init_expr.as_mut(), hook);
126                            Persist::Hoc(Hoc {
127                                insert,
128                                reg,
129                                hook: None,
130                            })
131                        } else {
132                            res
133                        }
134                    }
135                    _ => Persist::None,
136                };
137            }
138        }
139        Persist::None
140    }
141
142    fn get_persistent_id_from_possible_hoc(
143        &self,
144        call_expr: &mut CallExpr,
145        mut reg: Vec<(Ident, Id)>,
146        hook_reg: &mut HookRegister,
147    ) -> Persist {
148        let first_arg = match call_expr.args.as_mut_slice() {
149            [first, ..] => &mut first.expr,
150            _ => return Persist::None,
151        };
152        let callee = if let Callee::Expr(expr) = &call_expr.callee {
153            expr
154        } else {
155            return Persist::None;
156        };
157        let hoc_name = match callee.as_ref() {
158            Expr::Ident(fn_name) => fn_name.sym.to_string(),
159            // original react implement use `getSource` so we just follow them
160            Expr::Member(member) => self.cm.span_to_snippet(member.span).unwrap_or_default(),
161            _ => return Persist::None,
162        };
163        let reg_str = (
164            format!("{}${}", reg.last().unwrap().1 .0, &hoc_name).into(),
165            SyntaxContext::empty(),
166        );
167        match first_arg.as_mut() {
168            Expr::Call(expr) => {
169                let reg_ident = private_ident!("_c");
170                reg.push((reg_ident.clone(), reg_str));
171                if let Persist::Hoc(hoc) =
172                    self.get_persistent_id_from_possible_hoc(expr, reg, hook_reg)
173                {
174                    let mut first = first_arg.take();
175                    if let Some(HocHook { callee, rest_arg }) = &hoc.hook {
176                        let span = first.span();
177                        let mut args = vec![first.as_arg()];
178                        args.extend(rest_arg.clone());
179                        first = CallExpr {
180                            span,
181                            callee: callee.clone(),
182                            args,
183                            ..Default::default()
184                        }
185                        .into()
186                    }
187                    *first_arg = Box::new(make_assign_stmt(reg_ident, first));
188
189                    Persist::Hoc(hoc)
190                } else {
191                    Persist::None
192                }
193            }
194            Expr::Fn(_) | Expr::Arrow(_) => {
195                let reg_ident = private_ident!("_c");
196                let mut first = first_arg.take();
197                first.visit_mut_with(hook_reg);
198                let hook = if let Expr::Call(call) = first.as_ref() {
199                    let res = Some(HocHook {
200                        callee: call.callee.clone(),
201                        rest_arg: call.args[1..].to_owned(),
202                    });
203                    *first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first));
204                    res
205                } else {
206                    *first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first));
207                    None
208                };
209                reg.push((reg_ident, reg_str));
210                Persist::Hoc(Hoc {
211                    reg,
212                    insert: true,
213                    hook,
214                })
215            }
216            // export default hoc(Foo)
217            // const X = hoc(Foo)
218            Expr::Ident(ident) => {
219                if let Persist::Component(_) = get_persistent_id(ident) {
220                    Persist::Hoc(Hoc {
221                        reg,
222                        insert: true,
223                        hook: None,
224                    })
225                } else {
226                    Persist::None
227                }
228            }
229            _ => Persist::None,
230        }
231    }
232}
233
234/// We let user do /* @refresh reset */ to reset state in the whole file.
235impl<C> Visit for Refresh<C>
236where
237    C: Comments,
238{
239    fn visit_span(&mut self, n: &Span) {
240        if self.should_reset {
241            return;
242        }
243
244        let mut should_refresh = self.should_reset;
245        if let Some(comments) = &self.comments {
246            if !n.hi.is_dummy() {
247                comments.with_leading(n.hi - BytePos(1), |comments| {
248                    if comments.iter().any(|c| c.text.contains("@refresh reset")) {
249                        should_refresh = true
250                    }
251                });
252            }
253
254            comments.with_leading(n.lo, |comments| {
255                if comments.iter().any(|c| c.text.contains("@refresh reset")) {
256                    should_refresh = true
257                }
258            });
259
260            comments.with_trailing(n.lo, |comments| {
261                if comments.iter().any(|c| c.text.contains("@refresh reset")) {
262                    should_refresh = true
263                }
264            });
265        }
266
267        self.should_reset = should_refresh;
268    }
269}
270
271// TODO: figure out if we can insert all registers at once
272impl<C: Comments> VisitMut for Refresh<C> {
273    // Does anyone write react without esmodule?
274    // fn visit_mut_script(&mut self, _: &mut Script) {}
275
276    fn visit_mut_module(&mut self, n: &mut Module) {
277        if !self.enable {
278            return;
279        }
280
281        // to collect comments
282        self.visit_module(n);
283
284        self.visit_mut_module_items(&mut n.body);
285    }
286
287    fn visit_mut_module_items(&mut self, module_items: &mut Vec<ModuleItem>) {
288        let used_in_jsx = collect_ident_in_jsx(module_items);
289
290        let mut items = Vec::with_capacity(module_items.len());
291        let mut refresh_regs = Vec::<(Ident, Id)>::new();
292
293        let mut hook_visitor = HookRegister {
294            options: &self.options,
295            ident: Vec::new(),
296            extra_stmt: Vec::new(),
297            current_scope: vec![SyntaxContext::empty().apply_mark(self.global_mark)],
298            cm: &self.cm,
299            should_reset: self.should_reset,
300        };
301
302        for mut item in module_items.take() {
303            let persistent_id = match &mut item {
304                // function Foo() {}
305                ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { ident, .. }))) => {
306                    get_persistent_id(ident)
307                }
308
309                // export function Foo() {}
310                ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
311                    decl: Decl::Fn(FnDecl { ident, .. }),
312                    ..
313                })) => get_persistent_id(ident),
314
315                // export default function Foo() {}
316                ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
317                    decl:
318                        DefaultDecl::Fn(FnExpr {
319                            // We don't currently handle anonymous default exports.
320                            ident: Some(ident),
321                            ..
322                        }),
323                    ..
324                })) => get_persistent_id(ident),
325
326                // const Foo = () => {}
327                // export const Foo = () => {}
328                ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl)))
329                | ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
330                    decl: Decl::Var(var_decl),
331                    ..
332                })) => {
333                    self.get_persistent_id_from_var_decl(var_decl, &used_in_jsx, &mut hook_visitor)
334                }
335
336                // This code path handles nested cases like:
337                // export default memo(() => {})
338                // In those cases it is more plausible people will omit names
339                // so they're worth handling despite possible false positives.
340                ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
341                    expr,
342                    span,
343                })) => {
344                    if let Expr::Call(call) = expr.as_mut() {
345                        if let Persist::Hoc(Hoc { reg, hook, .. }) = self
346                            .get_persistent_id_from_possible_hoc(
347                                call,
348                                vec![(
349                                    private_ident!("_c"),
350                                    ("%default%".into(), SyntaxContext::empty()),
351                                )],
352                                &mut hook_visitor,
353                            )
354                        {
355                            if let Some(hook) = hook {
356                                make_hook_reg(expr.as_mut(), hook)
357                            }
358                            item = ExportDefaultExpr {
359                                expr: Box::new(make_assign_stmt(reg[0].0.clone(), expr.take())),
360                                span: *span,
361                            }
362                            .into();
363                            Persist::Hoc(Hoc {
364                                insert: false,
365                                reg,
366                                hook: None,
367                            })
368                        } else {
369                            Persist::None
370                        }
371                    } else {
372                        Persist::None
373                    }
374                }
375
376                _ => Persist::None,
377            };
378
379            if let Persist::Hoc(_) = persistent_id {
380                // we need to make hook transform happens after component for
381                // HOC
382                items.push(item);
383            } else {
384                item.visit_mut_children_with(&mut hook_visitor);
385
386                items.push(item);
387                items.extend(
388                    hook_visitor
389                        .extra_stmt
390                        .take()
391                        .into_iter()
392                        .map(ModuleItem::Stmt),
393                );
394            }
395
396            match persistent_id {
397                Persist::None => (),
398                Persist::Component(persistent_id) => {
399                    let registration_handle = private_ident!("_c");
400
401                    refresh_regs.push((registration_handle.clone(), persistent_id.to_id()));
402
403                    items.push(
404                        ExprStmt {
405                            span: DUMMY_SP,
406                            expr: Box::new(make_assign_stmt(
407                                registration_handle,
408                                persistent_id.into(),
409                            )),
410                        }
411                        .into(),
412                    );
413                }
414
415                Persist::Hoc(mut hoc) => {
416                    hoc.reg = hoc.reg.into_iter().rev().collect();
417                    if hoc.insert {
418                        let (ident, name) = hoc.reg.last().unwrap();
419                        items.push(
420                            ExprStmt {
421                                span: DUMMY_SP,
422                                expr: Box::new(make_assign_stmt(
423                                    ident.clone(),
424                                    Ident::new(name.0.clone(), DUMMY_SP, name.1).into(),
425                                )),
426                            }
427                            .into(),
428                        )
429                    }
430                    refresh_regs.append(&mut hoc.reg);
431                }
432            }
433        }
434
435        if !hook_visitor.ident.is_empty() {
436            items.insert(0, hook_visitor.gen_hook_handle().into());
437        }
438
439        // Insert
440        // ```
441        // var _c, _c1;
442        // ```
443        if !refresh_regs.is_empty() {
444            items.push(
445                VarDecl {
446                    span: DUMMY_SP,
447                    kind: VarDeclKind::Var,
448                    declare: false,
449                    decls: refresh_regs
450                        .iter()
451                        .map(|(handle, _)| VarDeclarator {
452                            span: DUMMY_SP,
453                            name: handle.clone().into(),
454                            init: None,
455                            definite: false,
456                        })
457                        .collect(),
458                    ..Default::default()
459                }
460                .into(),
461            );
462        }
463
464        // Insert
465        // ```
466        // $RefreshReg$(_c, "Hello");
467        // $RefreshReg$(_c1, "Foo");
468        // ```
469        let refresh_reg = self.options.refresh_reg.as_str();
470        for (handle, persistent_id) in refresh_regs {
471            items.push(
472                ExprStmt {
473                    span: DUMMY_SP,
474                    expr: CallExpr {
475                        callee: quote_ident!(refresh_reg).as_callee(),
476                        args: vec![handle.as_arg(), quote_str!(persistent_id.0).as_arg()],
477                        ..Default::default()
478                    }
479                    .into(),
480                }
481                .into(),
482            );
483        }
484
485        *module_items = items
486    }
487
488    fn visit_mut_ts_module_decl(&mut self, _: &mut TsModuleDecl) {}
489}
490
491fn make_hook_reg(expr: &mut Expr, mut hook: HocHook) {
492    let span = expr.span();
493    let mut args = vec![expr.take().as_arg()];
494    args.append(&mut hook.rest_arg);
495    *expr = CallExpr {
496        span,
497        callee: hook.callee,
498        args,
499        ..Default::default()
500    }
501    .into();
502}