mirror of
https://github.com/swc-project/swc.git
synced 2024-11-24 02:06:08 +03:00
refactor(es/react/fast-refresh): Use VisitMut
(#3129)
This commit is contained in:
parent
2ab65c2cea
commit
f8f04e031e
523
crates/swc_ecma_transforms_react/src/refresh/hook.rs
Normal file
523
crates/swc_ecma_transforms_react/src/refresh/hook.rs
Normal file
@ -0,0 +1,523 @@
|
||||
use std::mem;
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use sha1::{Digest, Sha1};
|
||||
use swc_atoms::JsWord;
|
||||
use swc_common::{util::take::Take, SourceMap, Spanned, DUMMY_SP};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_utils::{private_ident, quote_ident, ExprFactory};
|
||||
use swc_ecma_visit::{
|
||||
noop_visit_mut_type, noop_visit_type, Visit, VisitMut, VisitMutWith, VisitWith,
|
||||
};
|
||||
|
||||
use crate::RefreshOptions;
|
||||
|
||||
use super::util::{is_builtin_hook, make_call_expr, make_call_stmt, CollectIdent};
|
||||
|
||||
// function that use hooks
|
||||
struct HookSig {
|
||||
handle: Ident,
|
||||
// need to add an extra register, or alreay inlined
|
||||
hooks: Vec<Hook>,
|
||||
}
|
||||
|
||||
impl HookSig {
|
||||
fn new(hooks: Vec<Hook>) -> Self {
|
||||
HookSig {
|
||||
handle: private_ident!("_s"),
|
||||
hooks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Hook {
|
||||
callee: HookCall,
|
||||
key: String,
|
||||
}
|
||||
|
||||
// we only consider two kinds of callee as hook call
|
||||
enum HookCall {
|
||||
Ident(Ident),
|
||||
Member(Expr, Ident), // for obj and prop
|
||||
}
|
||||
pub struct HookRegister<'a> {
|
||||
pub options: &'a RefreshOptions,
|
||||
pub ident: Vec<Ident>,
|
||||
pub extra_stmt: Vec<Stmt>,
|
||||
pub scope_binding: IndexSet<JsWord>,
|
||||
pub cm: &'a SourceMap,
|
||||
pub should_reset: bool,
|
||||
}
|
||||
|
||||
impl<'a> HookRegister<'a> {
|
||||
pub fn gen_hook_handle(&mut self) -> Stmt {
|
||||
Stmt::Decl(Decl::Var(VarDecl {
|
||||
span: DUMMY_SP,
|
||||
kind: VarDeclKind::Var,
|
||||
decls: self
|
||||
.ident
|
||||
.take()
|
||||
.into_iter()
|
||||
.map(|id| VarDeclarator {
|
||||
span: DUMMY_SP,
|
||||
name: Pat::Ident(BindingIdent::from(id)),
|
||||
init: Some(Box::new(make_call_expr(quote_ident!(self
|
||||
.options
|
||||
.refresh_sig
|
||||
.clone())))),
|
||||
definite: false,
|
||||
})
|
||||
.collect(),
|
||||
declare: false,
|
||||
}))
|
||||
}
|
||||
|
||||
// The second call is around the function itself. This is used to associate a
|
||||
// type with a signature.
|
||||
// Unlike with $RefreshReg$, this needs to work for nested declarations too.
|
||||
fn wrap_with_register(&self, handle: Ident, func: Expr, hooks: Vec<Hook>) -> Expr {
|
||||
let mut args = vec![func.as_arg()];
|
||||
let mut sign = Vec::new();
|
||||
let mut custom_hook = Vec::new();
|
||||
|
||||
for hook in hooks {
|
||||
let name = match &hook.callee {
|
||||
HookCall::Ident(i) => i,
|
||||
HookCall::Member(_, i) => i,
|
||||
};
|
||||
sign.push(format!("{}{{{}}}", name.sym, hook.key));
|
||||
match &hook.callee {
|
||||
HookCall::Ident(ident) if !is_builtin_hook(ident) => {
|
||||
custom_hook.push(hook.callee);
|
||||
}
|
||||
HookCall::Member(obj, prop) if !is_builtin_hook(prop) => {
|
||||
if let Expr::Ident(ident) = obj {
|
||||
if ident.sym.as_ref() != "React" {
|
||||
custom_hook.push(hook.callee);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
// this is just for pass test
|
||||
let has_escape = sign.len() > 1;
|
||||
let sign = sign.join("\n");
|
||||
let sign = if self.options.emit_full_signatures {
|
||||
sign
|
||||
} else {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(sign);
|
||||
base64::encode(hasher.finalize())
|
||||
};
|
||||
|
||||
args.push(
|
||||
Expr::Lit(Lit::Str(Str {
|
||||
span: DUMMY_SP,
|
||||
value: sign.into(),
|
||||
has_escape,
|
||||
kind: StrKind::Synthesized,
|
||||
}))
|
||||
.as_arg(),
|
||||
);
|
||||
|
||||
let mut should_reset = self.should_reset;
|
||||
|
||||
let mut custom_hook_in_scope = Vec::new();
|
||||
for hook in custom_hook {
|
||||
let ident = match &hook {
|
||||
HookCall::Ident(ident) => Some(ident),
|
||||
HookCall::Member(Expr::Ident(ident), _) => Some(ident),
|
||||
_ => None,
|
||||
};
|
||||
if let None = ident.and_then(|id| self.scope_binding.get(&id.sym)) {
|
||||
// We don't have anything to put in the array because Hook is out of scope.
|
||||
// Since it could potentially have been edited, remount the component.
|
||||
should_reset = true;
|
||||
} else {
|
||||
custom_hook_in_scope.push(hook);
|
||||
}
|
||||
}
|
||||
|
||||
if should_reset || custom_hook_in_scope.len() > 0 {
|
||||
args.push(
|
||||
Expr::Lit(Lit::Bool(Bool {
|
||||
span: DUMMY_SP,
|
||||
value: should_reset,
|
||||
}))
|
||||
.as_arg(),
|
||||
);
|
||||
}
|
||||
|
||||
if custom_hook_in_scope.len() > 0 {
|
||||
let elems = custom_hook_in_scope
|
||||
.into_iter()
|
||||
.map(|hook| {
|
||||
Some(ExprOrSpread {
|
||||
spread: None,
|
||||
expr: Box::new(match hook {
|
||||
HookCall::Ident(ident) => Expr::Ident(ident),
|
||||
HookCall::Member(obj, prop) => Expr::Member(MemberExpr {
|
||||
span: DUMMY_SP,
|
||||
obj: ExprOrSuper::Expr(Box::new(obj)),
|
||||
prop: Box::new(Expr::Ident(prop)),
|
||||
computed: false,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
args.push(
|
||||
Expr::Fn(FnExpr {
|
||||
ident: None,
|
||||
function: Function {
|
||||
is_generator: false,
|
||||
is_async: false,
|
||||
params: Vec::new(),
|
||||
decorators: Vec::new(),
|
||||
span: DUMMY_SP,
|
||||
body: Some(BlockStmt {
|
||||
span: DUMMY_SP,
|
||||
stmts: vec![Stmt::Return(ReturnStmt {
|
||||
span: DUMMY_SP,
|
||||
arg: Some(Box::new(Expr::Array(ArrayLit {
|
||||
span: DUMMY_SP,
|
||||
elems,
|
||||
}))),
|
||||
})],
|
||||
}),
|
||||
type_params: None,
|
||||
return_type: None,
|
||||
},
|
||||
})
|
||||
.as_arg(),
|
||||
);
|
||||
}
|
||||
|
||||
Expr::Call(CallExpr {
|
||||
span: DUMMY_SP,
|
||||
callee: ExprOrSuper::Expr(Box::new(Expr::Ident(handle))),
|
||||
args,
|
||||
type_args: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn gen_hook_register_stmt(&mut self, ident: Ident, sig: HookSig) {
|
||||
self.ident.push(sig.handle.clone());
|
||||
self.extra_stmt.push(Stmt::Expr(ExprStmt {
|
||||
span: DUMMY_SP,
|
||||
expr: Box::new(self.wrap_with_register(sig.handle, Expr::Ident(ident), sig.hooks)),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> VisitMut for HookRegister<'a> {
|
||||
noop_visit_mut_type!();
|
||||
|
||||
fn visit_mut_block_stmt(&mut self, b: &mut BlockStmt) {
|
||||
let mut current_scope = IndexSet::new();
|
||||
|
||||
// TODO: merge with collect_decls
|
||||
for stmt in &b.stmts {
|
||||
stmt.collect_ident(&mut current_scope);
|
||||
}
|
||||
let orig_binding = self.scope_binding.len();
|
||||
self.scope_binding.extend(current_scope);
|
||||
let current_binding = self.scope_binding.len();
|
||||
|
||||
let old_ident = self.ident.take();
|
||||
let old_stmts = self.extra_stmt.take();
|
||||
|
||||
let stmt_count = b.stmts.len();
|
||||
let stmts = mem::replace(&mut b.stmts, Vec::with_capacity(stmt_count));
|
||||
|
||||
for mut stmt in stmts {
|
||||
stmt.visit_mut_children_with(self);
|
||||
self.scope_binding.truncate(current_binding);
|
||||
|
||||
b.stmts.push(stmt);
|
||||
b.stmts.append(&mut self.extra_stmt);
|
||||
}
|
||||
|
||||
if self.ident.len() > 0 {
|
||||
b.stmts.insert(0, self.gen_hook_handle())
|
||||
}
|
||||
|
||||
self.scope_binding.truncate(orig_binding);
|
||||
self.ident = old_ident;
|
||||
self.extra_stmt = old_stmts;
|
||||
}
|
||||
|
||||
fn visit_mut_expr(&mut self, e: &mut Expr) {
|
||||
e.visit_mut_children_with(self);
|
||||
|
||||
match e {
|
||||
Expr::Fn(FnExpr {
|
||||
function: Function {
|
||||
body: Some(body), ..
|
||||
},
|
||||
..
|
||||
}) => {
|
||||
let sig = collect_hooks(&mut body.stmts, self.cm);
|
||||
|
||||
if let Some(HookSig { handle, hooks }) = sig {
|
||||
self.ident.push(handle.clone());
|
||||
*e = self.wrap_with_register(handle, e.take(), hooks);
|
||||
}
|
||||
}
|
||||
Expr::Arrow(ArrowExpr { body, .. }) => {
|
||||
let sig = collect_hooks_arrow(body, self.cm);
|
||||
|
||||
if let Some(HookSig { handle, hooks }) = sig {
|
||||
self.ident.push(handle.clone());
|
||||
*e = self.wrap_with_register(handle, e.take(), hooks);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
|
||||
// we don't want visit_mut_expr to mess up with function name inference
|
||||
// so intercept it here
|
||||
|
||||
for decl in n.decls.iter_mut() {
|
||||
if let VarDeclarator {
|
||||
// it doesn't quite make sense for other Pat to appear here
|
||||
name: Pat::Ident(BindingIdent { id, .. }),
|
||||
init: Some(init),
|
||||
..
|
||||
} = decl
|
||||
{
|
||||
match init.as_mut() {
|
||||
Expr::Fn(FnExpr {
|
||||
function:
|
||||
Function {
|
||||
body: Some(body), ..
|
||||
},
|
||||
..
|
||||
}) => {
|
||||
if let Some(sig) = collect_hooks(&mut body.stmts, self.cm) {
|
||||
self.gen_hook_register_stmt(id.clone(), sig);
|
||||
}
|
||||
}
|
||||
Expr::Arrow(ArrowExpr { body, .. }) => {
|
||||
if let Some(sig) = collect_hooks_arrow(body, self.cm) {
|
||||
self.gen_hook_register_stmt(id.clone(), sig);
|
||||
}
|
||||
}
|
||||
_ => self.visit_mut_expr(init),
|
||||
}
|
||||
} else {
|
||||
decl.visit_mut_children_with(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_mut_default_decl(&mut self, d: &mut DefaultDecl) {
|
||||
d.visit_mut_children_with(self);
|
||||
|
||||
// only when expr has ident
|
||||
if let DefaultDecl::Fn(FnExpr {
|
||||
ident: Some(ident),
|
||||
function: Function {
|
||||
body: Some(body), ..
|
||||
},
|
||||
}) = d
|
||||
{
|
||||
if let Some(sig) = collect_hooks(&mut body.stmts, self.cm) {
|
||||
self.gen_hook_register_stmt(ident.clone(), sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_mut_fn_decl(&mut self, f: &mut FnDecl) {
|
||||
f.visit_mut_children_with(self);
|
||||
|
||||
if let Some(body) = &mut f.function.body {
|
||||
if let Some(sig) = collect_hooks(&mut body.stmts, self.cm) {
|
||||
self.gen_hook_register_stmt(f.ident.clone(), sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_hooks(stmts: &mut Vec<Stmt>, cm: &SourceMap) -> Option<HookSig> {
|
||||
let mut hook = HookCollector {
|
||||
state: Vec::new(),
|
||||
cm,
|
||||
};
|
||||
|
||||
stmts.visit_with(&mut hook);
|
||||
|
||||
if hook.state.len() > 0 {
|
||||
let sig = HookSig::new(hook.state);
|
||||
stmts.insert(0, make_call_stmt(sig.handle.clone()));
|
||||
|
||||
Some(sig)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_hooks_arrow(body: &mut BlockStmtOrExpr, cm: &SourceMap) -> Option<HookSig> {
|
||||
match body {
|
||||
BlockStmtOrExpr::BlockStmt(block) => collect_hooks(&mut block.stmts, cm),
|
||||
BlockStmtOrExpr::Expr(expr) => {
|
||||
let mut hook = HookCollector {
|
||||
state: Vec::new(),
|
||||
cm,
|
||||
};
|
||||
|
||||
expr.visit_with(&mut hook);
|
||||
|
||||
if hook.state.len() > 0 {
|
||||
let sig = HookSig::new(hook.state);
|
||||
*body = BlockStmtOrExpr::BlockStmt(BlockStmt {
|
||||
span: expr.span(),
|
||||
stmts: vec![
|
||||
make_call_stmt(sig.handle.clone()),
|
||||
Stmt::Return(ReturnStmt {
|
||||
span: expr.span(),
|
||||
arg: Some(Box::new(expr.as_mut().take())),
|
||||
}),
|
||||
],
|
||||
});
|
||||
Some(sig)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HookCollector<'a> {
|
||||
state: Vec<Hook>,
|
||||
cm: &'a SourceMap,
|
||||
}
|
||||
|
||||
static IS_HOOK_LIKE: Lazy<Regex> = Lazy::new(|| Regex::new("^use[A-Z]").unwrap());
|
||||
impl<'a> HookCollector<'a> {
|
||||
fn get_hook_from_call_expr(&self, expr: &CallExpr, lhs: Option<&Pat>) -> Option<Hook> {
|
||||
let callee = if let ExprOrSuper::Expr(callee) = &expr.callee {
|
||||
Some(callee.as_ref())
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
let mut hook_call = None;
|
||||
let ident = match callee {
|
||||
Expr::Ident(ident) => {
|
||||
hook_call = Some(HookCall::Ident(ident.clone()));
|
||||
Some(ident)
|
||||
}
|
||||
Expr::Member(MemberExpr {
|
||||
obj: ExprOrSuper::Expr(obj),
|
||||
prop,
|
||||
..
|
||||
}) => {
|
||||
if let Expr::Ident(ident) = prop.as_ref() {
|
||||
hook_call = Some(HookCall::Member(*obj.clone(), ident.clone()));
|
||||
Some(ident)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}?;
|
||||
let name = if IS_HOOK_LIKE.is_match(&ident.sym) {
|
||||
Some(ident)
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
let mut key = if let Some(name) = lhs {
|
||||
self.cm
|
||||
.span_to_snippet(name.span())
|
||||
.unwrap_or_else(|_| String::new())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// Some built-in Hooks reset on edits to arguments.
|
||||
if &name.sym == "useState" && expr.args.len() > 0 {
|
||||
// useState first argument is initial state.
|
||||
key += &format!(
|
||||
"({})",
|
||||
self.cm
|
||||
.span_to_snippet(expr.args[0].span())
|
||||
.unwrap_or_else(|_| String::new())
|
||||
);
|
||||
} else if &name.sym == "useReducer" && expr.args.len() > 1 {
|
||||
// useReducer second argument is initial state.
|
||||
key += &format!(
|
||||
"({})",
|
||||
self.cm
|
||||
.span_to_snippet(expr.args[1].span())
|
||||
.unwrap_or("".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
let callee = hook_call?;
|
||||
Some(Hook { callee, key })
|
||||
}
|
||||
|
||||
fn get_hook_from_expr(&self, expr: &Expr, lhs: Option<&Pat>) -> Option<Hook> {
|
||||
if let Expr::Call(call) = expr {
|
||||
self.get_hook_from_call_expr(call, lhs)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visit for HookCollector<'a> {
|
||||
noop_visit_type!();
|
||||
|
||||
fn visit_block_stmt_or_expr(&mut self, _: &BlockStmtOrExpr) {}
|
||||
|
||||
fn visit_block_stmt(&mut self, _: &BlockStmt) {}
|
||||
|
||||
fn visit_expr(&mut self, expr: &Expr) {
|
||||
expr.visit_children_with(self);
|
||||
|
||||
if let Expr::Call(call) = expr {
|
||||
if let Some(hook) = self.get_hook_from_call_expr(call, None) {
|
||||
self.state.push(hook)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||
match stmt {
|
||||
Stmt::Expr(ExprStmt { expr, .. }) => {
|
||||
if let Some(hook) = self.get_hook_from_expr(expr, None) {
|
||||
self.state.push(hook)
|
||||
} else {
|
||||
stmt.visit_children_with(self)
|
||||
}
|
||||
}
|
||||
Stmt::Decl(Decl::Var(var_decl)) => {
|
||||
for decl in &var_decl.decls {
|
||||
if let Some(init) = &decl.init {
|
||||
if let Some(hook) = self.get_hook_from_expr(init, Some(&decl.name)) {
|
||||
self.state.push(hook)
|
||||
} else {
|
||||
stmt.visit_children_with(self)
|
||||
}
|
||||
} else {
|
||||
stmt.visit_children_with(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::Return(ReturnStmt { arg: Some(arg), .. }) => {
|
||||
if let Some(hook) = self.get_hook_from_expr(arg.as_ref(), None) {
|
||||
self.state.push(hook)
|
||||
} else {
|
||||
stmt.visit_children_with(self)
|
||||
}
|
||||
}
|
||||
_ => stmt.visit_children_with(self),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +1,23 @@
|
||||
use self::util::{
|
||||
callee_should_ignore, gen_custom_hook_record, is_body_arrow_fn, is_builtin_hook,
|
||||
is_import_or_require, make_assign_stmt, make_call_expr, make_call_stmt, CollectIdent,
|
||||
use self::{
|
||||
hook::HookRegister,
|
||||
util::{
|
||||
collect_ident_in_jsx, is_body_arrow_fn, is_import_or_require, make_assign_stmt,
|
||||
CollectIdent,
|
||||
},
|
||||
};
|
||||
use base64;
|
||||
use indexmap::IndexSet;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
mem,
|
||||
};
|
||||
use swc_atoms::JsWord;
|
||||
use swc_common::{
|
||||
collections::{AHashMap, AHashSet},
|
||||
comments::Comments,
|
||||
sync::Lrc,
|
||||
util::take::Take,
|
||||
BytePos, SourceMap, Span, Spanned, SyntaxContext, DUMMY_SP,
|
||||
collections::AHashSet, comments::Comments, sync::Lrc, util::take::Take, BytePos, SourceMap,
|
||||
Span, Spanned, SyntaxContext, DUMMY_SP,
|
||||
};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_utils::{ident::IdentLike, private_ident, quote_ident, quote_str, Id};
|
||||
use swc_ecma_visit::{Fold, FoldWith, Visit};
|
||||
use swc_ecma_visit::{as_folder, Fold, Visit, VisitMut, VisitMutWith};
|
||||
|
||||
pub mod options;
|
||||
use options::RefreshOptions;
|
||||
mod hook;
|
||||
mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
@ -33,13 +26,11 @@ mod tests;
|
||||
struct Hoc {
|
||||
insert: bool,
|
||||
reg: Vec<(Ident, Id)>,
|
||||
// use to register hook for all level of HOC, first is hook register ident
|
||||
// rest is hook register call args
|
||||
hook: Option<HocHook>,
|
||||
}
|
||||
struct HocHook {
|
||||
ident: ExprOrSuper,
|
||||
args: Vec<ExprOrSpread>,
|
||||
callee: ExprOrSuper,
|
||||
rest_arg: Vec<ExprOrSpread>,
|
||||
}
|
||||
enum Persist {
|
||||
Hoc(Hoc),
|
||||
@ -59,55 +50,22 @@ fn get_persistent_id(ident: &Ident) -> Persist {
|
||||
}
|
||||
}
|
||||
|
||||
fn hook_to_handle_map(hook_fn: Vec<FnWithHook>) -> (AHashMap<Ident, FnWithHook>, AHashSet<Ident>) {
|
||||
let mut has_ident = HashMap::default();
|
||||
let mut ignore = HashSet::default();
|
||||
for hook in hook_fn {
|
||||
if let Some(binding) = &hook.binding {
|
||||
has_ident.insert(binding.clone(), hook);
|
||||
} else {
|
||||
ignore.insert(hook.handle);
|
||||
}
|
||||
}
|
||||
(has_ident, ignore)
|
||||
}
|
||||
// function that use hooks
|
||||
struct FnWithHook {
|
||||
binding: Option<Ident>, // ident of function
|
||||
handle: Ident, // variable to register
|
||||
hook: Vec<Hook>,
|
||||
}
|
||||
|
||||
struct Hook {
|
||||
name: Ident,
|
||||
callee: HookCall,
|
||||
key: String,
|
||||
}
|
||||
|
||||
// we only consider two kinds of expr as hook call
|
||||
enum HookCall {
|
||||
Ident(Ident),
|
||||
Member(Expr, Ident), // for obj and prop
|
||||
}
|
||||
|
||||
/// `react-refresh/babel`
|
||||
/// https://github.com/facebook/react/blob/master/packages/react-refresh/src/ReactFreshBabelPlugin.js
|
||||
/// https://github.com/facebook/react/blob/main/packages/react-refresh/src/ReactFreshBabelPlugin.js
|
||||
pub fn refresh<C: Comments>(
|
||||
dev: bool,
|
||||
options: Option<RefreshOptions>,
|
||||
cm: Lrc<SourceMap>,
|
||||
comments: Option<C>,
|
||||
) -> impl Fold {
|
||||
Refresh {
|
||||
) -> impl Fold + VisitMut {
|
||||
as_folder(Refresh {
|
||||
enable: dev && options.is_some(),
|
||||
cm,
|
||||
comments,
|
||||
should_reset: false,
|
||||
options: options.unwrap_or(Default::default()),
|
||||
used_in_jsx: HashSet::default(),
|
||||
curr_hook_fn: Vec::new(),
|
||||
scope_binding: IndexSet::new(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
struct Refresh<C: Comments> {
|
||||
@ -115,300 +73,17 @@ struct Refresh<C: Comments> {
|
||||
options: RefreshOptions,
|
||||
cm: Lrc<SourceMap>,
|
||||
should_reset: bool,
|
||||
used_in_jsx: AHashSet<JsWord>,
|
||||
comments: Option<C>,
|
||||
curr_hook_fn: Vec<FnWithHook>,
|
||||
// bindings in current and all parent scope
|
||||
scope_binding: IndexSet<JsWord>,
|
||||
}
|
||||
|
||||
static IS_HOOK_LIKE: Lazy<Regex> = Lazy::new(|| Regex::new("^use[A-Z]").unwrap());
|
||||
|
||||
impl<C: Comments> Refresh<C> {
|
||||
fn get_hook_from_call_expr(&self, expr: &CallExpr, lhs: Option<&Pat>) -> Option<Hook> {
|
||||
let callee = if let ExprOrSuper::Expr(callee) = &expr.callee {
|
||||
Some(callee.as_ref())
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
let mut hook_call = None;
|
||||
let ident = match callee {
|
||||
Expr::Ident(ident) => {
|
||||
hook_call = Some(HookCall::Ident(ident.clone()));
|
||||
Some(ident)
|
||||
}
|
||||
Expr::Member(MemberExpr {
|
||||
obj: ExprOrSuper::Expr(obj),
|
||||
prop,
|
||||
..
|
||||
}) => {
|
||||
if let Expr::Ident(ident) = prop.as_ref() {
|
||||
hook_call = Some(HookCall::Member(*obj.clone(), ident.clone()));
|
||||
Some(ident)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}?;
|
||||
let name = if IS_HOOK_LIKE.is_match(&ident.sym) {
|
||||
Some(ident.clone())
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
let mut key = if let Some(name) = lhs {
|
||||
self.cm
|
||||
.span_to_snippet(name.span())
|
||||
.unwrap_or("".to_string())
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
// Some built-in Hooks reset on edits to arguments.
|
||||
if &name.sym == "useState" && expr.args.len() > 0 {
|
||||
// useState second argument is initial state.
|
||||
key += &format!(
|
||||
"({})",
|
||||
self.cm
|
||||
.span_to_snippet(expr.args[0].span())
|
||||
.unwrap_or("".to_string())
|
||||
);
|
||||
} else if &name.sym == "useReducer" && expr.args.len() > 1 {
|
||||
// useReducer second argument is initial state.
|
||||
key += &format!(
|
||||
"({})",
|
||||
self.cm
|
||||
.span_to_snippet(expr.args[1].span())
|
||||
.unwrap_or("".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
let callee = hook_call?;
|
||||
Some(Hook { name, callee, key })
|
||||
}
|
||||
|
||||
fn get_hook_from_expr(&self, expr: &Expr, lhs: Option<&Pat>, reg: &mut Vec<Hook>) {
|
||||
match expr {
|
||||
Expr::Paren(ParenExpr { expr, .. }) => {
|
||||
if let Expr::Seq(SeqExpr { exprs, .. }) = expr.as_ref() {
|
||||
for expr in exprs {
|
||||
self.get_hook_from_expr(expr, lhs, reg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Seq(SeqExpr { exprs, .. }) => {
|
||||
for expr in exprs {
|
||||
self.get_hook_from_expr(expr, lhs, reg);
|
||||
}
|
||||
}
|
||||
Expr::Call(expr) => {
|
||||
if let Some(hook) = self.get_hook_from_call_expr(expr, lhs) {
|
||||
reg.push(hook);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_hook_sign(&mut self, body: &mut BlockStmt) -> Option<FnWithHook> {
|
||||
let mut sign_res = Vec::new();
|
||||
for stmt in &body.stmts {
|
||||
match stmt {
|
||||
Stmt::Expr(ExprStmt { expr, .. }) => {
|
||||
self.get_hook_from_expr(expr, None, &mut sign_res)
|
||||
}
|
||||
Stmt::Decl(Decl::Var(var_decl)) => {
|
||||
for decl in &var_decl.decls {
|
||||
if let Some(init) = &decl.init {
|
||||
self.get_hook_from_expr(init, Some(&decl.name), &mut sign_res);
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::Return(ReturnStmt { arg: Some(arg), .. }) => {
|
||||
self.get_hook_from_expr(arg.as_ref(), None, &mut sign_res)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// The signature call is split in two parts. One part is called inside the
|
||||
// function. This is used to signal when first render happens.
|
||||
if sign_res.len() != 0 {
|
||||
let handle = private_ident!("_s");
|
||||
body.stmts.insert(0, make_call_stmt(handle.clone()));
|
||||
Some(FnWithHook {
|
||||
binding: None,
|
||||
handle,
|
||||
hook: sign_res,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_hook_sign_from_arrow(&mut self, body: &mut BlockStmtOrExpr) -> Option<FnWithHook> {
|
||||
match body {
|
||||
BlockStmtOrExpr::BlockStmt(stmt) => self.get_hook_sign(stmt),
|
||||
BlockStmtOrExpr::Expr(expr) => {
|
||||
let mut hook = Vec::new();
|
||||
self.get_hook_from_expr(expr, None, &mut hook);
|
||||
if hook.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut block = BlockStmt {
|
||||
span: expr.span(),
|
||||
stmts: Vec::new(),
|
||||
};
|
||||
let handle = private_ident!("_s");
|
||||
block.stmts.push(make_call_stmt(handle.clone()));
|
||||
block.stmts.push(Stmt::Return(ReturnStmt {
|
||||
span: expr.span(),
|
||||
arg: Some(Box::new(expr.as_mut().take())),
|
||||
}));
|
||||
|
||||
*body = BlockStmtOrExpr::BlockStmt(block);
|
||||
|
||||
Some(FnWithHook {
|
||||
binding: None,
|
||||
handle,
|
||||
hook,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_hook_handle(&self, curr_hook_fn: &Vec<FnWithHook>) -> Stmt {
|
||||
let refresh_sig = self.options.refresh_sig.as_str();
|
||||
Stmt::Decl(Decl::Var(VarDecl {
|
||||
span: DUMMY_SP,
|
||||
kind: VarDeclKind::Var,
|
||||
decls: curr_hook_fn
|
||||
.iter()
|
||||
.map(|FnWithHook { ref handle, .. }| VarDeclarator {
|
||||
span: DUMMY_SP,
|
||||
name: Pat::Ident(BindingIdent::from(handle.clone())),
|
||||
init: Some(Box::new(make_call_expr(quote_ident!(refresh_sig)))),
|
||||
definite: false,
|
||||
})
|
||||
.collect(),
|
||||
declare: false,
|
||||
}))
|
||||
}
|
||||
|
||||
// The second call is around the function itself. This is used to associate a
|
||||
// type with a signature.
|
||||
// Unlike with $RefreshReg$, this needs to work for nested declarations too.
|
||||
fn gen_hook_register(&self, func: Expr, hook_fn: &mut FnWithHook) -> Expr {
|
||||
let mut args = vec![func];
|
||||
let mut sign = Vec::new();
|
||||
let hooks = mem::take(&mut hook_fn.hook);
|
||||
let mut custom_hook = Vec::new();
|
||||
|
||||
for hook in hooks {
|
||||
sign.push(format!("{}{{{}}}", hook.name.sym, hook.key));
|
||||
match &hook.callee {
|
||||
HookCall::Ident(ident) if !is_builtin_hook(ident) => {
|
||||
custom_hook.push(hook.callee);
|
||||
}
|
||||
HookCall::Member(obj, prop) if !is_builtin_hook(prop) => {
|
||||
if let Expr::Ident(ident) = obj {
|
||||
if ident.sym.as_ref() != "React" {
|
||||
custom_hook.push(hook.callee);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
// this is just for pass test
|
||||
let has_escape = sign.len() > 1;
|
||||
let sign = sign.join("\n");
|
||||
let sign = if self.options.emit_full_signatures {
|
||||
sign
|
||||
} else {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(sign);
|
||||
base64::encode(hasher.finalize())
|
||||
};
|
||||
|
||||
args.push(Expr::Lit(Lit::Str(Str {
|
||||
span: DUMMY_SP,
|
||||
value: sign.into(),
|
||||
has_escape,
|
||||
kind: StrKind::Synthesized,
|
||||
})));
|
||||
|
||||
let mut should_reset = self.should_reset;
|
||||
|
||||
let mut custom_hook_in_scope = Vec::new();
|
||||
for hook in custom_hook {
|
||||
let ident = match &hook {
|
||||
HookCall::Ident(ident) => Some(ident),
|
||||
HookCall::Member(Expr::Ident(ident), _) => Some(ident),
|
||||
_ => None,
|
||||
};
|
||||
if let None = ident.and_then(|id| self.scope_binding.get(&id.sym)) {
|
||||
// We don't have anything to put in the array because Hook is out ofscope.
|
||||
// Since it could potentially have been edited, remount the component.
|
||||
should_reset = true;
|
||||
} else {
|
||||
custom_hook_in_scope.push(hook);
|
||||
}
|
||||
}
|
||||
|
||||
if should_reset || custom_hook_in_scope.len() > 0 {
|
||||
args.push(Expr::Lit(Lit::Bool(Bool {
|
||||
span: DUMMY_SP,
|
||||
value: should_reset,
|
||||
})));
|
||||
}
|
||||
|
||||
if custom_hook_in_scope.len() > 0 {
|
||||
let elems = custom_hook_in_scope
|
||||
.into_iter()
|
||||
.map(|hook| {
|
||||
Some(ExprOrSpread {
|
||||
spread: None,
|
||||
expr: Box::new(match hook {
|
||||
HookCall::Ident(ident) => Expr::Ident(ident),
|
||||
HookCall::Member(obj, prop) => Expr::Member(MemberExpr {
|
||||
span: DUMMY_SP,
|
||||
obj: ExprOrSuper::Expr(Box::new(obj)),
|
||||
prop: Box::new(Expr::Ident(prop)),
|
||||
computed: false,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
args.push(gen_custom_hook_record(elems));
|
||||
}
|
||||
|
||||
Expr::Call(CallExpr {
|
||||
span: DUMMY_SP,
|
||||
callee: ExprOrSuper::Expr(Box::new(Expr::Ident(hook_fn.handle.clone()))),
|
||||
args: args
|
||||
.into_iter()
|
||||
.map(|arg| ExprOrSpread {
|
||||
spread: None,
|
||||
expr: Box::new(arg),
|
||||
})
|
||||
.collect(),
|
||||
type_args: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn gen_hook_register_stmt(&self, func: Expr, hook_fn: &mut FnWithHook) -> Stmt {
|
||||
Stmt::Expr(ExprStmt {
|
||||
span: DUMMY_SP,
|
||||
expr: Box::new(self.gen_hook_register(func, hook_fn)),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_persistent_id_from_var_decl(
|
||||
&self,
|
||||
var_decl: &mut VarDecl,
|
||||
ignore: &AHashSet<Ident>,
|
||||
used_in_jsx: &AHashSet<JsWord>,
|
||||
hook_reg: &mut HookRegister,
|
||||
) -> Persist {
|
||||
// We only handle the case when a single variable is declared
|
||||
if let [VarDeclarator {
|
||||
@ -417,7 +92,7 @@ impl<C: Comments> Refresh<C> {
|
||||
..
|
||||
}] = var_decl.decls.as_mut_slice()
|
||||
{
|
||||
if self.used_in_jsx.contains(&binding.id.sym) && !is_import_or_require(init_expr) {
|
||||
if used_in_jsx.contains(&binding.id.sym) && !is_import_or_require(init_expr) {
|
||||
match init_expr.as_ref() {
|
||||
// TaggedTpl is for something like styled.div`...`
|
||||
Expr::Arrow(_) | Expr::Fn(_) | Expr::TaggedTpl(_) | Expr::Call(_) => {
|
||||
@ -440,11 +115,28 @@ impl<C: Comments> Refresh<C> {
|
||||
}
|
||||
}
|
||||
// Maybe a HOC.
|
||||
Expr::Call(call_expr) => self.get_persistent_id_from_possible_hoc(
|
||||
call_expr,
|
||||
vec![(private_ident!("_c"), persistent_id.to_id())],
|
||||
ignore,
|
||||
),
|
||||
Expr::Call(call_expr) => {
|
||||
let res = self.get_persistent_id_from_possible_hoc(
|
||||
call_expr,
|
||||
vec![(private_ident!("_c"), persistent_id.to_id())],
|
||||
hook_reg,
|
||||
);
|
||||
if let Persist::Hoc(Hoc {
|
||||
insert,
|
||||
reg,
|
||||
hook: Some(hook),
|
||||
}) = res
|
||||
{
|
||||
make_hook_reg(init_expr.as_mut(), hook);
|
||||
Persist::Hoc(Hoc {
|
||||
insert,
|
||||
reg,
|
||||
hook: None,
|
||||
})
|
||||
} else {
|
||||
res
|
||||
}
|
||||
}
|
||||
_ => Persist::None,
|
||||
};
|
||||
}
|
||||
@ -456,23 +148,8 @@ impl<C: Comments> Refresh<C> {
|
||||
&self,
|
||||
call_expr: &mut CallExpr,
|
||||
mut reg: Vec<(Ident, Id)>,
|
||||
// sadly unlike orignal implent our transform for component happens before hook
|
||||
// so we should just ignore hook register
|
||||
ignore: &AHashSet<Ident>,
|
||||
hook_reg: &mut HookRegister,
|
||||
) -> Persist {
|
||||
if let Some(ident) = callee_should_ignore(ignore, &call_expr.callee) {
|
||||
// there's at least one item in reg
|
||||
return if reg.len() > 1 {
|
||||
let args = call_expr.args[1..].to_vec();
|
||||
Persist::Hoc(Hoc {
|
||||
reg,
|
||||
insert: true,
|
||||
hook: Some(HocHook { ident, args }),
|
||||
})
|
||||
} else {
|
||||
Persist::None
|
||||
};
|
||||
};
|
||||
let first_arg = match call_expr.args.as_mut_slice() {
|
||||
[first, ..] => &mut first.expr,
|
||||
_ => return Persist::None,
|
||||
@ -497,24 +174,24 @@ impl<C: Comments> Refresh<C> {
|
||||
let reg_ident = private_ident!("_c");
|
||||
reg.push((reg_ident.clone(), reg_str));
|
||||
if let Persist::Hoc(hoc) =
|
||||
self.get_persistent_id_from_possible_hoc(expr, reg, ignore)
|
||||
self.get_persistent_id_from_possible_hoc(expr, reg, hook_reg)
|
||||
{
|
||||
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first_arg.take()));
|
||||
if let Some(hook) = &hoc.hook {
|
||||
let span = call_expr.span;
|
||||
let first = ExprOrSpread {
|
||||
let mut first = first_arg.take();
|
||||
if let Some(HocHook { callee, rest_arg }) = &hoc.hook {
|
||||
let span = first.span();
|
||||
let mut args = vec![ExprOrSpread {
|
||||
spread: None,
|
||||
expr: Box::new(Expr::Call(call_expr.take())),
|
||||
};
|
||||
let mut args = vec![first];
|
||||
args.extend(hook.args.clone());
|
||||
*call_expr = CallExpr {
|
||||
expr: first,
|
||||
}];
|
||||
args.extend(rest_arg.clone());
|
||||
first = Box::new(Expr::Call(CallExpr {
|
||||
span,
|
||||
callee: hook.ident.clone(),
|
||||
callee: callee.clone(),
|
||||
args,
|
||||
type_args: None,
|
||||
};
|
||||
}))
|
||||
}
|
||||
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first));
|
||||
|
||||
Persist::Hoc(hoc)
|
||||
} else {
|
||||
@ -523,12 +200,24 @@ impl<C: Comments> Refresh<C> {
|
||||
}
|
||||
Expr::Fn(_) | Expr::Arrow(_) => {
|
||||
let reg_ident = private_ident!("_c");
|
||||
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first_arg.take()));
|
||||
let mut first = first_arg.take();
|
||||
first.visit_mut_with(hook_reg);
|
||||
let hook = if let Expr::Call(call) = first.as_ref() {
|
||||
let res = Some(HocHook {
|
||||
callee: call.callee.clone(),
|
||||
rest_arg: call.args[1..].to_owned(),
|
||||
});
|
||||
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first));
|
||||
res
|
||||
} else {
|
||||
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first));
|
||||
None
|
||||
};
|
||||
reg.push((reg_ident, reg_str));
|
||||
Persist::Hoc(Hoc {
|
||||
reg,
|
||||
insert: true,
|
||||
hook: None,
|
||||
hook,
|
||||
})
|
||||
}
|
||||
// export default hoc(Foo)
|
||||
@ -549,6 +238,7 @@ impl<C: Comments> Refresh<C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// We let user do /* @refresh reset */ to reset state in the whole file.
|
||||
impl<C> Visit for Refresh<C>
|
||||
where
|
||||
C: Comments,
|
||||
@ -579,208 +269,46 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// We let user do /* @refresh reset */ to reset state in the whole file.
|
||||
///
|
||||
/// TODO: VisitMut
|
||||
impl<C: Comments> Fold for Refresh<C> {
|
||||
fn fold_block_stmt(&mut self, n: BlockStmt) -> BlockStmt {
|
||||
let mut current_scope = IndexSet::new();
|
||||
// TODO: figure out if we can insert all registers at once
|
||||
impl<C: Comments> VisitMut for Refresh<C> {
|
||||
// Does anyone write react without esmodule?
|
||||
// fn visit_mut_script(&mut self, _: &mut Script) {}
|
||||
|
||||
for stmt in &n.stmts {
|
||||
stmt.collect_ident(&mut current_scope);
|
||||
}
|
||||
let orig_bindings = self.scope_binding.len();
|
||||
self.scope_binding.extend(current_scope.into_iter());
|
||||
|
||||
let orig_hook = mem::take(&mut self.curr_hook_fn);
|
||||
let mut n = n.fold_children_with(self);
|
||||
let curr_hook = mem::replace(&mut self.curr_hook_fn, orig_hook);
|
||||
self.scope_binding.truncate(orig_bindings);
|
||||
|
||||
if curr_hook.len() > 0 {
|
||||
let stmt_count = n.stmts.len();
|
||||
let stmts = mem::replace(&mut n.stmts, Vec::with_capacity(stmt_count));
|
||||
n.stmts.push(self.gen_hook_handle(&curr_hook));
|
||||
let (mut handle_map, _) = hook_to_handle_map(curr_hook);
|
||||
|
||||
for stmt in stmts {
|
||||
let mut reg = Vec::new();
|
||||
match &stmt {
|
||||
Stmt::Decl(Decl::Fn(FnDecl { ident, .. })) => {
|
||||
if let Some(hook) = handle_map.remove(ident) {
|
||||
reg.push((ident.clone(), hook));
|
||||
}
|
||||
}
|
||||
Stmt::Decl(Decl::Var(VarDecl { decls, .. })) => {
|
||||
for decl in decls {
|
||||
if let Pat::Ident(BindingIdent { id, .. }) = &decl.name {
|
||||
if let Some(hook) = handle_map.remove(id) {
|
||||
reg.push((id.clone(), hook));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
n.stmts.push(stmt);
|
||||
if reg.len() > 0 {
|
||||
for (ident, mut hook) in reg {
|
||||
n.stmts
|
||||
.push(self.gen_hook_register_stmt(Expr::Ident(ident), &mut hook));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
n
|
||||
}
|
||||
|
||||
fn fold_call_expr(&mut self, n: CallExpr) -> CallExpr {
|
||||
let n = n.fold_children_with(self);
|
||||
|
||||
if let ExprOrSuper::Expr(expr) = &n.callee {
|
||||
let ident = match expr.as_ref() {
|
||||
Expr::Ident(ident) => ident,
|
||||
Expr::Member(MemberExpr { prop, .. }) => {
|
||||
if let Expr::Ident(ident) = prop.as_ref() {
|
||||
ident
|
||||
} else {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
_ => return n,
|
||||
};
|
||||
match ident.sym.as_ref() {
|
||||
"createElement" | "jsx" | "jsxDEV" | "jsxs" => {
|
||||
if let Some(ExprOrSpread { expr, .. }) = n.args.get(0) {
|
||||
if let Expr::Ident(ident) = expr.as_ref() {
|
||||
self.used_in_jsx.insert(ident.sym.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
fn fold_default_decl(&mut self, n: DefaultDecl) -> DefaultDecl {
|
||||
let mut n = n.fold_children_with(self);
|
||||
|
||||
// arrow function is handled in fold_expr
|
||||
if let DefaultDecl::Fn(FnExpr {
|
||||
// original implent somehow doesn't handle unnamed default export either
|
||||
ident: Some(ident),
|
||||
function: Function {
|
||||
body: Some(body), ..
|
||||
},
|
||||
}) = &mut n
|
||||
{
|
||||
if let Some(mut hook) = self.get_hook_sign(body) {
|
||||
hook.binding = Some(ident.clone());
|
||||
self.curr_hook_fn.push(hook);
|
||||
}
|
||||
}
|
||||
|
||||
n
|
||||
}
|
||||
|
||||
fn fold_expr(&mut self, n: Expr) -> Expr {
|
||||
let n = n.fold_children_with(self);
|
||||
|
||||
match n {
|
||||
Expr::Fn(mut func) => {
|
||||
if let Some(mut hook) = func
|
||||
.function
|
||||
.body
|
||||
.as_mut()
|
||||
.and_then(|body| self.get_hook_sign(body))
|
||||
{
|
||||
// I don't believe many people would use it, but anyway
|
||||
let mut should_remove = false;
|
||||
if let Some(ident) = &func.ident {
|
||||
self.scope_binding.insert(ident.sym.clone());
|
||||
should_remove = true;
|
||||
};
|
||||
let reg = self.gen_hook_register(Expr::Fn(func), &mut hook);
|
||||
if should_remove {
|
||||
self.scope_binding.truncate(self.scope_binding.len() - 1);
|
||||
};
|
||||
self.curr_hook_fn.push(hook);
|
||||
reg
|
||||
} else {
|
||||
Expr::Fn(func)
|
||||
}
|
||||
}
|
||||
Expr::Arrow(mut func) => {
|
||||
if let Some(mut hook) = self.get_hook_sign_from_arrow(&mut func.body) {
|
||||
let reg = self.gen_hook_register(Expr::Arrow(func), &mut hook);
|
||||
self.curr_hook_fn.push(hook);
|
||||
reg
|
||||
} else {
|
||||
Expr::Arrow(func)
|
||||
}
|
||||
}
|
||||
Expr::Call(call) => Expr::Call(call),
|
||||
_ => n,
|
||||
}
|
||||
}
|
||||
|
||||
fn fold_fn_decl(&mut self, n: FnDecl) -> FnDecl {
|
||||
let mut n = n.fold_children_with(self);
|
||||
|
||||
if let Some(mut hook) = n
|
||||
.function
|
||||
.body
|
||||
.as_mut()
|
||||
.and_then(|body| self.get_hook_sign(body))
|
||||
{
|
||||
hook.binding = Some(n.ident.clone());
|
||||
self.curr_hook_fn.push(hook);
|
||||
}
|
||||
|
||||
n
|
||||
}
|
||||
|
||||
fn fold_jsx_opening_element(&mut self, n: JSXOpeningElement) -> JSXOpeningElement {
|
||||
if let JSXElementName::Ident(ident) = &n.name {
|
||||
self.used_in_jsx.insert(ident.sym.clone());
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
fn fold_module_items(&mut self, module_items: Vec<ModuleItem>) -> Vec<ModuleItem> {
|
||||
fn visit_mut_module_items(&mut self, module_items: &mut Vec<ModuleItem>) {
|
||||
if !self.enable {
|
||||
return module_items;
|
||||
return;
|
||||
}
|
||||
|
||||
self.visit_module_items(&module_items);
|
||||
// to collect comments
|
||||
self.visit_module_items(module_items);
|
||||
|
||||
for item in &module_items {
|
||||
item.collect_ident(&mut self.scope_binding);
|
||||
let mut current_scope = IndexSet::new();
|
||||
// TODO: merge with collect_decls
|
||||
for item in module_items.iter() {
|
||||
item.collect_ident(&mut current_scope);
|
||||
}
|
||||
|
||||
let module_items = module_items.fold_children_with(self);
|
||||
let current_binding = self.scope_binding.len();
|
||||
|
||||
// TODO: false positive, doesn't consider identity shadow
|
||||
let used_in_jsx = collect_ident_in_jsx(module_items);
|
||||
|
||||
let mut items = Vec::with_capacity(module_items.len());
|
||||
let mut refresh_regs = Vec::<(Ident, Id)>::new();
|
||||
|
||||
if self.curr_hook_fn.len() > 0 {
|
||||
items.push(ModuleItem::Stmt(self.gen_hook_handle(&self.curr_hook_fn)));
|
||||
}
|
||||
|
||||
let curr_hook_fn = mem::take(&mut self.curr_hook_fn);
|
||||
let (mut handle_map, ignore) = hook_to_handle_map(curr_hook_fn);
|
||||
|
||||
for mut item in module_items {
|
||||
let mut hook_reg = Vec::new();
|
||||
let mut hook_visitor = HookRegister {
|
||||
options: &self.options,
|
||||
ident: Vec::new(),
|
||||
extra_stmt: Vec::new(),
|
||||
scope_binding: current_scope,
|
||||
cm: &self.cm,
|
||||
should_reset: self.should_reset,
|
||||
};
|
||||
|
||||
for mut item in module_items.take() {
|
||||
let persistent_id = match &mut item {
|
||||
// function Foo() {}
|
||||
ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { ident, .. }))) => {
|
||||
if let Some(hook) = handle_map.remove(&ident) {
|
||||
hook_reg.push((ident.clone(), hook));
|
||||
}
|
||||
get_persistent_id(ident)
|
||||
}
|
||||
|
||||
@ -788,12 +316,7 @@ impl<C: Comments> Fold for Refresh<C> {
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
|
||||
decl: Decl::Fn(FnDecl { ident, .. }),
|
||||
..
|
||||
})) => {
|
||||
if let Some(hook) = handle_map.remove(&ident) {
|
||||
hook_reg.push((ident.clone(), hook));
|
||||
}
|
||||
get_persistent_id(ident)
|
||||
}
|
||||
})) => get_persistent_id(ident),
|
||||
|
||||
// export default function Foo() {}
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
|
||||
@ -804,12 +327,7 @@ impl<C: Comments> Fold for Refresh<C> {
|
||||
..
|
||||
}),
|
||||
..
|
||||
})) => {
|
||||
if let Some(hook) = handle_map.remove(&ident) {
|
||||
hook_reg.push((ident.clone(), hook));
|
||||
};
|
||||
get_persistent_id(ident)
|
||||
}
|
||||
})) => get_persistent_id(ident),
|
||||
|
||||
// const Foo = () => {}
|
||||
// export const Foo = () => {}
|
||||
@ -818,14 +336,7 @@ impl<C: Comments> Fold for Refresh<C> {
|
||||
decl: Decl::Var(var_decl),
|
||||
..
|
||||
})) => {
|
||||
for decl in &var_decl.decls {
|
||||
if let Pat::Ident(BindingIdent { id, .. }) = &decl.name {
|
||||
if let Some(hook) = handle_map.remove(&id) {
|
||||
hook_reg.push((id.clone(), hook));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.get_persistent_id_from_var_decl(var_decl, &ignore)
|
||||
self.get_persistent_id_from_var_decl(var_decl, &used_in_jsx, &mut hook_visitor)
|
||||
}
|
||||
|
||||
// This code path handles nested cases like:
|
||||
@ -837,16 +348,19 @@ impl<C: Comments> Fold for Refresh<C> {
|
||||
span,
|
||||
})) => {
|
||||
if let Expr::Call(call) = expr.as_mut() {
|
||||
if let Persist::Hoc(Hoc { reg, .. }) = self
|
||||
if let Persist::Hoc(Hoc { reg, hook, .. }) = self
|
||||
.get_persistent_id_from_possible_hoc(
|
||||
call,
|
||||
vec![(
|
||||
private_ident!("_c"),
|
||||
("%default%".into(), SyntaxContext::empty()),
|
||||
)],
|
||||
&ignore,
|
||||
&mut hook_visitor,
|
||||
)
|
||||
{
|
||||
if let Some(hook) = hook {
|
||||
make_hook_reg(expr.as_mut(), hook)
|
||||
}
|
||||
item = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(
|
||||
ExportDefaultExpr {
|
||||
expr: Box::new(make_assign_stmt(reg[0].0.clone(), expr.take())),
|
||||
@ -869,12 +383,22 @@ impl<C: Comments> Fold for Refresh<C> {
|
||||
_ => Persist::None,
|
||||
};
|
||||
|
||||
items.push(item);
|
||||
if let Persist::Hoc(_) = persistent_id {
|
||||
// we need to make hook transform happens after component for
|
||||
// HOC
|
||||
items.push(item);
|
||||
} else {
|
||||
item.visit_mut_children_with(&mut hook_visitor);
|
||||
self.scope_binding.truncate(current_binding);
|
||||
|
||||
for (ident, mut hook) in hook_reg {
|
||||
items.push(ModuleItem::Stmt(
|
||||
self.gen_hook_register_stmt(Expr::Ident(ident), &mut hook),
|
||||
))
|
||||
items.push(item);
|
||||
items.extend(
|
||||
hook_visitor
|
||||
.extra_stmt
|
||||
.take()
|
||||
.into_iter()
|
||||
.map(ModuleItem::Stmt),
|
||||
);
|
||||
}
|
||||
|
||||
match persistent_id {
|
||||
@ -913,6 +437,10 @@ impl<C: Comments> Fold for Refresh<C> {
|
||||
}
|
||||
}
|
||||
|
||||
if hook_visitor.ident.len() > 0 {
|
||||
items.insert(0, ModuleItem::Stmt(hook_visitor.gen_hook_handle()));
|
||||
}
|
||||
|
||||
// Insert
|
||||
// ```
|
||||
// var _c, _c1;
|
||||
@ -961,71 +489,23 @@ impl<C: Comments> Fold for Refresh<C> {
|
||||
})));
|
||||
}
|
||||
|
||||
items
|
||||
*module_items = items
|
||||
}
|
||||
|
||||
fn fold_ts_module_decl(&mut self, n: TsModuleDecl) -> TsModuleDecl {
|
||||
n
|
||||
}
|
||||
|
||||
fn fold_var_decl(&mut self, n: VarDecl) -> VarDecl {
|
||||
let VarDecl {
|
||||
span,
|
||||
kind,
|
||||
declare,
|
||||
decls,
|
||||
} = n;
|
||||
|
||||
// we don't want fold_expr to mess up with function name inference
|
||||
// so intercept it here
|
||||
let decls = decls
|
||||
.into_iter()
|
||||
.map(|decl| match decl {
|
||||
VarDeclarator {
|
||||
span,
|
||||
// it doesn't quite make sense for other Pat to appear here
|
||||
name: Pat::Ident(BindingIdent { id, type_ann }),
|
||||
init: Some(init),
|
||||
definite,
|
||||
} => {
|
||||
let init = match *init {
|
||||
Expr::Fn(mut expr) => {
|
||||
if let Some(mut hook) = expr
|
||||
.function
|
||||
.body
|
||||
.as_mut()
|
||||
.and_then(|body| self.get_hook_sign(body))
|
||||
{
|
||||
hook.binding = Some(id.clone());
|
||||
self.curr_hook_fn.push(hook);
|
||||
}
|
||||
Expr::Fn(expr.fold_children_with(self))
|
||||
}
|
||||
Expr::Arrow(mut expr) => {
|
||||
if let Some(mut hook) = self.get_hook_sign_from_arrow(&mut expr.body) {
|
||||
hook.binding = Some(id.clone());
|
||||
self.curr_hook_fn.push(hook);
|
||||
}
|
||||
Expr::Arrow(expr.fold_children_with(self))
|
||||
}
|
||||
_ => self.fold_expr(*init),
|
||||
};
|
||||
VarDeclarator {
|
||||
span,
|
||||
name: Pat::Ident(BindingIdent { id, type_ann }),
|
||||
init: Some(Box::new(init)),
|
||||
definite,
|
||||
}
|
||||
}
|
||||
_ => decl.fold_children_with(self),
|
||||
})
|
||||
.collect();
|
||||
|
||||
VarDecl {
|
||||
span,
|
||||
kind,
|
||||
declare,
|
||||
decls,
|
||||
}
|
||||
}
|
||||
fn visit_mut_ts_module_decl(&mut self, _: &mut TsModuleDecl) {}
|
||||
}
|
||||
|
||||
fn make_hook_reg(expr: &mut Expr, mut hook: HocHook) {
|
||||
let span = expr.span();
|
||||
let mut args = vec![ExprOrSpread {
|
||||
spread: None,
|
||||
expr: Box::new(expr.take()),
|
||||
}];
|
||||
args.append(&mut hook.rest_arg);
|
||||
*expr = Expr::Call(CallExpr {
|
||||
span,
|
||||
callee: hook.callee,
|
||||
args,
|
||||
type_args: None,
|
||||
});
|
||||
}
|
||||
|
@ -697,6 +697,34 @@ test!(
|
||||
"#
|
||||
);
|
||||
|
||||
test!(
|
||||
ignore,
|
||||
::swc_ecma_parser::Syntax::Es(::swc_ecma_parser::EsConfig {
|
||||
jsx: true,
|
||||
..Default::default()
|
||||
}),
|
||||
tr,
|
||||
register_identifiers_used_in_jsx_false_positive,
|
||||
r#"
|
||||
const A = foo() {}
|
||||
const B = () => {
|
||||
const A = () => null
|
||||
return <A />
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
const A = foo() {}
|
||||
const B = () => {
|
||||
const A = () => null
|
||||
return <A />
|
||||
}
|
||||
|
||||
_c = B;
|
||||
var _c;
|
||||
$RefreshReg$(_c, "B");
|
||||
"#
|
||||
);
|
||||
|
||||
test!(
|
||||
::swc_ecma_parser::Syntax::Es(::swc_ecma_parser::EsConfig {
|
||||
jsx: true,
|
||||
@ -1428,3 +1456,31 @@ test!(
|
||||
$RefreshReg$(_c1, 'Comp');
|
||||
"
|
||||
);
|
||||
|
||||
test!(
|
||||
Default::default(),
|
||||
tr,
|
||||
issue_2261,
|
||||
"
|
||||
export function App() {
|
||||
console.log(useState())
|
||||
|
||||
return null;
|
||||
}
|
||||
",
|
||||
r#"
|
||||
var _s = $RefreshSig$();
|
||||
|
||||
export function App() {
|
||||
_s();
|
||||
console.log(useState())
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_s(App, "useState{}");
|
||||
_c = App;
|
||||
var _c;
|
||||
$RefreshReg$(_c, "App")
|
||||
"#
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ use indexmap::IndexSet;
|
||||
use swc_atoms::JsWord;
|
||||
use swc_common::{collections::AHashSet, Spanned, SyntaxContext, DUMMY_SP};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
|
||||
|
||||
pub trait CollectIdent {
|
||||
fn collect_ident(&self, collection: &mut IndexSet<JsWord>);
|
||||
@ -215,46 +216,48 @@ pub fn is_import_or_require(expr: &Expr) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn callee_should_ignore<'a>(
|
||||
ignore: &'a AHashSet<Ident>,
|
||||
callee: &ExprOrSuper,
|
||||
) -> Option<ExprOrSuper> {
|
||||
let expr = if let ExprOrSuper::Expr(expr) = callee {
|
||||
Some(expr)
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
let ident = if let Expr::Ident(ident) = expr.as_ref() {
|
||||
Some(ident)
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
ignore
|
||||
.get(ident)
|
||||
.map(|ident| ExprOrSuper::Expr(Box::new(Expr::Ident(ident.clone()))))
|
||||
pub struct UsedInJsx(AHashSet<JsWord>);
|
||||
|
||||
impl Visit for UsedInJsx {
|
||||
noop_visit_type!();
|
||||
|
||||
fn visit_call_expr(&mut self, n: &CallExpr) {
|
||||
n.visit_children_with(self);
|
||||
|
||||
if let ExprOrSuper::Expr(expr) = &n.callee {
|
||||
let ident = match expr.as_ref() {
|
||||
Expr::Ident(ident) => ident,
|
||||
Expr::Member(MemberExpr { prop, .. }) => {
|
||||
if let Expr::Ident(ident) = prop.as_ref() {
|
||||
ident
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
if matches!(
|
||||
ident.sym.as_ref(),
|
||||
"createElement" | "jsx" | "jsxDEV" | "jsxs"
|
||||
) {
|
||||
if let Some(ExprOrSpread { expr, .. }) = n.args.get(0) {
|
||||
if let Expr::Ident(ident) = expr.as_ref() {
|
||||
self.0.insert(ident.sym.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_jsx_opening_element(&mut self, n: &JSXOpeningElement) {
|
||||
if let JSXElementName::Ident(ident) = &n.name {
|
||||
self.0.insert(ident.sym.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gen_custom_hook_record(elems: Vec<Option<ExprOrSpread>>) -> Expr {
|
||||
Expr::Fn(FnExpr {
|
||||
ident: None,
|
||||
function: Function {
|
||||
is_generator: false,
|
||||
is_async: false,
|
||||
params: Vec::new(),
|
||||
decorators: Vec::new(),
|
||||
span: DUMMY_SP,
|
||||
body: Some(BlockStmt {
|
||||
span: DUMMY_SP,
|
||||
stmts: vec![Stmt::Return(ReturnStmt {
|
||||
span: DUMMY_SP,
|
||||
arg: Some(Box::new(Expr::Array(ArrayLit {
|
||||
span: DUMMY_SP,
|
||||
elems,
|
||||
}))),
|
||||
})],
|
||||
}),
|
||||
type_params: None,
|
||||
return_type: None,
|
||||
},
|
||||
})
|
||||
pub fn collect_ident_in_jsx<V: VisitWith<UsedInJsx>>(item: &V) -> AHashSet<JsWord> {
|
||||
let mut visitor = UsedInJsx(AHashSet::default());
|
||||
item.visit_with(&mut visitor);
|
||||
visitor.0
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user