swc_ecma_transforms_react/refresh/
hook.rs1use std::{fmt::Write, mem};
2
3use base64::prelude::{Engine, BASE64_STANDARD};
4use sha1::{Digest, Sha1};
5use swc_common::{util::take::Take, SourceMap, SourceMapper, Spanned, SyntaxContext, DUMMY_SP};
6use swc_ecma_ast::*;
7use swc_ecma_utils::{private_ident, quote_ident, ExprFactory};
8use swc_ecma_visit::{
9 noop_visit_mut_type, noop_visit_type, Visit, VisitMut, VisitMutWith, VisitWith,
10};
11
12use super::util::{is_builtin_hook, make_call_expr, make_call_stmt};
13use crate::RefreshOptions;
14
15struct HookSig {
17 handle: Ident,
18 hooks: Vec<Hook>,
20}
21
22impl HookSig {
23 fn new(hooks: Vec<Hook>) -> Self {
24 HookSig {
25 handle: private_ident!("_s"),
26 hooks,
27 }
28 }
29}
30
31struct Hook {
32 callee: HookCall,
33 key: String,
34}
35
36#[allow(clippy::large_enum_variant)]
38enum HookCall {
39 Ident(Ident),
40 Member(Expr, IdentName), }
42pub struct HookRegister<'a> {
43 pub options: &'a RefreshOptions,
44 pub ident: Vec<Ident>,
45 pub extra_stmt: Vec<Stmt>,
46 pub current_scope: Vec<SyntaxContext>,
47 pub cm: &'a SourceMap,
48 pub should_reset: bool,
49}
50
51impl HookRegister<'_> {
52 pub fn gen_hook_handle(&mut self) -> Stmt {
53 VarDecl {
54 span: DUMMY_SP,
55 kind: VarDeclKind::Var,
56 decls: self
57 .ident
58 .take()
59 .into_iter()
60 .map(|id| VarDeclarator {
61 span: DUMMY_SP,
62 name: id.into(),
63 init: Some(Box::new(make_call_expr(
64 quote_ident!(self.options.refresh_sig.clone()).into(),
65 ))),
66 definite: false,
67 })
68 .collect(),
69 declare: false,
70 ..Default::default()
71 }
72 .into()
73 }
74
75 fn wrap_with_register(&self, handle: Ident, func: Expr, hooks: Vec<Hook>) -> Expr {
79 let mut args = vec![func.as_arg()];
80 let mut sign = Vec::new();
81 let mut custom_hook = Vec::new();
82
83 for hook in hooks {
84 let name = match &hook.callee {
85 HookCall::Ident(i) => i.clone(),
86 HookCall::Member(_, i) => i.clone().into(),
87 };
88 sign.push(format!("{}{{{}}}", name.sym, hook.key));
89 match &hook.callee {
90 HookCall::Ident(ident) if !is_builtin_hook(&ident.sym) => {
91 custom_hook.push(hook.callee);
92 }
93 HookCall::Member(Expr::Ident(obj_ident), prop) if !is_builtin_hook(&prop.sym) => {
94 if obj_ident.sym.as_ref() != "React" {
95 custom_hook.push(hook.callee);
96 }
97 }
98 _ => (),
99 };
100 }
101
102 let sign = sign.join("\n");
103 let sign = if self.options.emit_full_signatures {
104 sign
105 } else {
106 let mut hasher = Sha1::new();
107 hasher.update(sign);
108 BASE64_STANDARD.encode(hasher.finalize())
109 };
110
111 args.push(
112 Lit::Str(Str {
113 span: DUMMY_SP,
114 raw: None,
115 value: sign.into(),
116 })
117 .as_arg(),
118 );
119
120 let mut should_reset = self.should_reset;
121
122 let mut custom_hook_in_scope = Vec::new();
123
124 for hook in custom_hook {
125 let ident = match &hook {
126 HookCall::Ident(ident) => Some(ident),
127 HookCall::Member(Expr::Ident(ident), _) => Some(ident),
128 _ => None,
129 };
130 if !ident
131 .map(|id| self.current_scope.contains(&id.ctxt))
132 .unwrap_or(false)
133 {
134 should_reset = true;
137 } else {
138 custom_hook_in_scope.push(hook);
139 }
140 }
141
142 if should_reset || !custom_hook_in_scope.is_empty() {
143 args.push(should_reset.as_arg());
144 }
145
146 if !custom_hook_in_scope.is_empty() {
147 let elems = custom_hook_in_scope
148 .into_iter()
149 .map(|hook| {
150 Some(
151 match hook {
152 HookCall::Ident(ident) => Expr::from(ident),
153 HookCall::Member(obj, prop) => MemberExpr {
154 span: DUMMY_SP,
155 obj: Box::new(obj),
156 prop: MemberProp::Ident(prop),
157 }
158 .into(),
159 }
160 .as_arg(),
161 )
162 })
163 .collect();
164 args.push(
165 Function {
166 is_generator: false,
167 is_async: false,
168 params: Vec::new(),
169 decorators: Vec::new(),
170 span: DUMMY_SP,
171 body: Some(BlockStmt {
172 span: DUMMY_SP,
173 stmts: vec![Stmt::Return(ReturnStmt {
174 span: DUMMY_SP,
175 arg: Some(Box::new(Expr::Array(ArrayLit {
176 span: DUMMY_SP,
177 elems,
178 }))),
179 })],
180 ..Default::default()
181 }),
182 ..Default::default()
183 }
184 .as_arg(),
185 );
186 }
187
188 CallExpr {
189 span: DUMMY_SP,
190 callee: handle.as_callee(),
191 args,
192 ..Default::default()
193 }
194 .into()
195 }
196
197 fn gen_hook_register_stmt(&mut self, ident: Ident, sig: HookSig) {
198 self.ident.push(sig.handle.clone());
199 self.extra_stmt.push(
200 ExprStmt {
201 span: DUMMY_SP,
202 expr: Box::new(self.wrap_with_register(sig.handle, ident.into(), sig.hooks)),
203 }
204 .into(),
205 )
206 }
207}
208
209impl VisitMut for HookRegister<'_> {
210 noop_visit_mut_type!();
211
212 fn visit_mut_block_stmt(&mut self, b: &mut BlockStmt) {
213 let old_ident = self.ident.take();
214 let old_stmts = self.extra_stmt.take();
215
216 self.current_scope.push(b.ctxt);
217
218 let stmt_count = b.stmts.len();
219 let stmts = mem::replace(&mut b.stmts, Vec::with_capacity(stmt_count));
220
221 for mut stmt in stmts {
222 stmt.visit_mut_children_with(self);
223
224 b.stmts.push(stmt);
225 b.stmts.append(&mut self.extra_stmt);
226 }
227
228 if !self.ident.is_empty() {
229 b.stmts.insert(0, self.gen_hook_handle())
230 }
231
232 self.current_scope.pop();
233 self.ident = old_ident;
234 self.extra_stmt = old_stmts;
235 }
236
237 fn visit_mut_expr(&mut self, e: &mut Expr) {
238 e.visit_mut_children_with(self);
239
240 match e {
241 Expr::Fn(FnExpr { function: f, .. }) if f.body.is_some() => {
242 let sig = collect_hooks(&mut f.body.as_mut().unwrap().stmts, self.cm);
243
244 if let Some(HookSig { handle, hooks }) = sig {
245 self.ident.push(handle.clone());
246 *e = self.wrap_with_register(handle, e.take(), hooks);
247 }
248 }
249 Expr::Arrow(ArrowExpr { body, .. }) => {
250 let sig = collect_hooks_arrow(body, self.cm);
251
252 if let Some(HookSig { handle, hooks }) = sig {
253 self.ident.push(handle.clone());
254 *e = self.wrap_with_register(handle, e.take(), hooks);
255 }
256 }
257 _ => (),
258 }
259 }
260
261 fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
262 for decl in n.decls.iter_mut() {
266 if let VarDeclarator {
267 name: Pat::Ident(id),
269 init: Some(init),
270 ..
271 } = decl
272 {
273 match init.as_mut() {
274 Expr::Fn(FnExpr { function: f, .. }) if f.body.is_some() => {
275 f.body.visit_mut_with(self);
276 if let Some(sig) =
277 collect_hooks(&mut f.body.as_mut().unwrap().stmts, self.cm)
278 {
279 self.gen_hook_register_stmt(Ident::from(&*id), sig);
280 }
281 }
282 Expr::Arrow(ArrowExpr { body, .. }) => {
283 body.visit_mut_with(self);
284 if let Some(sig) = collect_hooks_arrow(body, self.cm) {
285 self.gen_hook_register_stmt(Ident::from(&*id), sig);
286 }
287 }
288 _ => self.visit_mut_expr(init),
289 }
290 } else {
291 decl.visit_mut_children_with(self)
292 }
293 }
294 }
295
296 fn visit_mut_default_decl(&mut self, d: &mut DefaultDecl) {
297 d.visit_mut_children_with(self);
298
299 match d {
301 DefaultDecl::Fn(FnExpr {
302 ident: Some(ident),
303 function: f,
304 }) if f.body.is_some() => {
305 if let Some(sig) = collect_hooks(&mut f.body.as_mut().unwrap().stmts, self.cm) {
306 self.gen_hook_register_stmt(ident.clone(), sig);
307 }
308 }
309 _ => {}
310 }
311 }
312
313 fn visit_mut_fn_decl(&mut self, f: &mut FnDecl) {
314 f.visit_mut_children_with(self);
315
316 if let Some(body) = &mut f.function.body {
317 if let Some(sig) = collect_hooks(&mut body.stmts, self.cm) {
318 self.gen_hook_register_stmt(f.ident.clone(), sig);
319 }
320 }
321 }
322}
323
324fn collect_hooks(stmts: &mut Vec<Stmt>, cm: &SourceMap) -> Option<HookSig> {
325 let mut hook = HookCollector {
326 state: Vec::new(),
327 cm,
328 };
329
330 stmts.visit_with(&mut hook);
331
332 if !hook.state.is_empty() {
333 let sig = HookSig::new(hook.state);
334 stmts.insert(0, make_call_stmt(sig.handle.clone()));
335
336 Some(sig)
337 } else {
338 None
339 }
340}
341
342fn collect_hooks_arrow(body: &mut BlockStmtOrExpr, cm: &SourceMap) -> Option<HookSig> {
343 match body {
344 BlockStmtOrExpr::BlockStmt(block) => collect_hooks(&mut block.stmts, cm),
345 BlockStmtOrExpr::Expr(expr) => {
346 let mut hook = HookCollector {
347 state: Vec::new(),
348 cm,
349 };
350
351 expr.visit_with(&mut hook);
352
353 if !hook.state.is_empty() {
354 let sig = HookSig::new(hook.state);
355 *body = BlockStmtOrExpr::BlockStmt(BlockStmt {
356 span: expr.span(),
357 stmts: vec![
358 make_call_stmt(sig.handle.clone()),
359 Stmt::Return(ReturnStmt {
360 span: expr.span(),
361 arg: Some(Box::new(expr.as_mut().take())),
362 }),
363 ],
364 ..Default::default()
365 });
366 Some(sig)
367 } else {
368 None
369 }
370 }
371 #[cfg(swc_ast_unknown)]
372 _ => None,
373 }
374}
375
376struct HookCollector<'a> {
377 state: Vec<Hook>,
378 cm: &'a SourceMap,
379}
380
381fn is_hook_like(s: &str) -> bool {
382 if let Some(s) = s.strip_prefix("use") {
383 s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
384 } else {
385 false
386 }
387}
388
389impl HookCollector<'_> {
390 fn get_hook_from_call_expr(&self, expr: &CallExpr, lhs: Option<&Pat>) -> Option<Hook> {
391 let callee = if let Callee::Expr(callee) = &expr.callee {
392 Some(callee.as_ref())
393 } else {
394 None
395 }?;
396 let mut hook_call = None;
397 let ident = match callee {
398 Expr::Ident(ident) => {
399 hook_call = Some(HookCall::Ident(ident.clone()));
400 Some(&ident.sym)
401 }
402 Expr::Member(MemberExpr {
404 obj,
405 prop: MemberProp::Ident(ident),
406 ..
407 }) => {
408 hook_call = Some(HookCall::Member(*obj.clone(), ident.clone()));
409 Some(&ident.sym)
410 }
411 _ => None,
412 }?;
413 let name = if is_hook_like(ident) {
414 Some(ident)
415 } else {
416 None
417 }?;
418 let mut key = if let Some(name) = lhs {
419 self.cm.span_to_snippet(name.span()).unwrap_or_default()
420 } else {
421 String::new()
422 };
423 if *name == "useState" && !expr.args.is_empty() {
425 let _ = write!(
427 key,
428 "({})",
429 self.cm
430 .span_to_snippet(expr.args[0].span())
431 .unwrap_or_default()
432 );
433 } else if name == "useReducer" && expr.args.len() > 1 {
434 let _ = write!(
436 key,
437 "({})",
438 self.cm
439 .span_to_snippet(expr.args[1].span())
440 .unwrap_or_default()
441 );
442 }
443
444 let callee = hook_call?;
445 Some(Hook { callee, key })
446 }
447
448 fn get_hook_from_expr(&self, expr: &Expr, lhs: Option<&Pat>) -> Option<Hook> {
449 if let Expr::Call(call) = expr {
450 self.get_hook_from_call_expr(call, lhs)
451 } else {
452 None
453 }
454 }
455}
456
457impl Visit for HookCollector<'_> {
458 noop_visit_type!();
459
460 fn visit_block_stmt_or_expr(&mut self, _: &BlockStmtOrExpr) {}
461
462 fn visit_block_stmt(&mut self, _: &BlockStmt) {}
463
464 fn visit_expr(&mut self, expr: &Expr) {
465 expr.visit_children_with(self);
466
467 if let Expr::Call(call) = expr {
468 if let Some(hook) = self.get_hook_from_call_expr(call, None) {
469 self.state.push(hook)
470 }
471 }
472 }
473
474 fn visit_stmt(&mut self, stmt: &Stmt) {
475 match stmt {
476 Stmt::Expr(ExprStmt { expr, .. }) => {
477 if let Some(hook) = self.get_hook_from_expr(expr, None) {
478 self.state.push(hook)
479 } else {
480 stmt.visit_children_with(self)
481 }
482 }
483 Stmt::Decl(Decl::Var(var_decl)) => {
484 for decl in &var_decl.decls {
485 if let Some(init) = &decl.init {
486 if let Some(hook) = self.get_hook_from_expr(init, Some(&decl.name)) {
487 self.state.push(hook)
488 } else {
489 stmt.visit_children_with(self)
490 }
491 } else {
492 stmt.visit_children_with(self)
493 }
494 }
495 }
496 Stmt::Return(ReturnStmt { arg: Some(arg), .. }) => {
497 if let Some(hook) = self.get_hook_from_expr(arg.as_ref(), None) {
498 self.state.push(hook)
499 } else {
500 stmt.visit_children_with(self)
501 }
502 }
503 _ => stmt.visit_children_with(self),
504 }
505 }
506}