swc_ecma_transforms_react/refresh/
mod.rs

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