use std::path::{Path, PathBuf};
use rustc_hash::FxHashSet;
use swc_atoms::Atom;
use swc_common::errors::HANDLER;
use swc_common::source_map::DefaultSourceMapGenConfig;
use swc_common::{sync::Lrc, BytePos, LineCol, SourceMap, Span, SyntaxContext, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_codegen::{text_writer::JsWriter, Config as EmitConfig, Emitter};
use swc_ecma_utils::ExprFactory;
use swc_ecma_visit::{Visit, VisitMut, VisitMutWith, VisitWith};
use crate::closure::{
collect_closure_vars, collect_closure_vars_arrow, collect_pat_bindings, ClosureCtx,
DeclCollector,
};
use crate::factory::{
assign_member, const_named_fn, const_recur_decl, id, ident_expr, num_lit, shorthand_prop,
str_lit,
};
use crate::gestures::{contains_gesture_obj, is_layout_anim_chain};
use crate::globals::{DEFAULT_GLOBALS, FORCE_SKIP_CAPTURE};
use once_cell::sync::Lazy;
static FORCE_SKIP_CAPTURE_ATOMS: Lazy<FxHashSet<Atom>> =
Lazy::new(|| FORCE_SKIP_CAPTURE.iter().map(|s| Atom::from(*s)).collect());
use crate::hash::worklet_hash;
use crate::hooks::{
function_hooks, is_object_hook, GESTURE_BUILDER_METHODS, LAYOUT_ANIM_CALLBACKS,
};
use crate::inline_style::warn_obj;
use crate::options::WorkletsOptions;
use indexmap::IndexSet;
const WORKLET_DIRECTIVE: &str = "worklet";
const UNKNOWN_VERSION: &str = "unknown";
const CONTEXT_OBJECT_MARKER: &str = "__workletContextObject";
const CONTEXT_OBJECT_FACTORY: &str = "__workletContextObjectFactory";
const WORKLET_CLASS_MARKER: &str = "__workletClass";
const GENERATED_WORKLETS_DIR: &str = ".worklets";
const BUNDLE_MODE_UNSUPPORTED_MESSAGE: &str =
"react-native-worklets bundleMode is not supported by swc-react-native-worklets";
const WORKLET_CLASS_FACTORY_SUFFIX: &str = "__classFactory";
pub struct WorkletsVisitor {
pub options: WorkletsOptions,
pub filename: String,
pub worklet_number: u32,
pub is_release: bool,
pub skip_file: bool,
pub globals: FxHashSet<Atom>,
pub file_bindings: FxHashSet<Atom>,
pub source_map: Option<Lrc<SourceMap>>,
unsupported_bundle_mode_reported: bool,
pending_prepends: Vec<Stmt>,
}
impl WorkletsVisitor {
pub fn new(options: WorkletsOptions) -> Self {
let mut globals: FxHashSet<Atom> = DEFAULT_GLOBALS.iter().map(|s| Atom::from(*s)).collect();
for g in &options.globals {
globals.insert(Atom::from(g.as_str()));
}
let filename = options.filename.clone().unwrap_or_default();
let is_release = options.is_release;
let skip_file = is_generated_worklet_file(&filename);
Self {
options,
filename,
worklet_number: 1,
is_release,
skip_file,
globals,
file_bindings: FxHashSet::default(),
source_map: None,
unsupported_bundle_mode_reported: false,
pending_prepends: vec![],
}
}
pub fn with_source_map(mut self, cm: Lrc<SourceMap>) -> Self {
self.source_map = Some(cm);
self
}
fn closure_ctx(&self) -> ClosureCtx<'_> {
ClosureCtx {
globals: &self.globals,
file_bindings: &self.file_bindings,
force_skip_capture: &FORCE_SKIP_CAPTURE_ATOMS,
strict_global: self.options.strict_global,
bundle_mode: self.options.bundle_mode,
}
}
fn plugin_version(&self) -> &str {
if self.options.plugin_version.is_empty() {
UNKNOWN_VERSION
} else {
self.options.plugin_version.as_str()
}
}
fn report_unsupported_bundle_mode(&mut self, span: Span) {
if self.unsupported_bundle_mode_reported {
return;
}
self.unsupported_bundle_mode_reported = true;
if HANDLER.is_set() {
HANDLER.with(|handler| {
handler
.struct_span_err(span, BUNDLE_MODE_UNSUPPORTED_MESSAGE)
.emit()
});
}
}
fn location_path(&self) -> String {
if self.filename.is_empty() {
return String::new();
}
if !self.options.relative_source_location {
return to_posix_path(&self.filename);
}
let cwd = match self.options.cwd.as_deref() {
Some(c) => PathBuf::from(c),
None => match std::env::current_dir() {
Ok(c) => c,
Err(_) => return to_posix_path(&self.filename),
},
};
let abs = PathBuf::from(&self.filename);
let location = abs
.strip_prefix(&cwd)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| self.filename.clone());
to_posix_path(&location)
}
fn source_name(&self) -> String {
if self.filename.is_empty() {
return "unknownFile".to_string();
}
let base = Path::new(&self.filename)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknownFile")
.to_string();
let parts: Vec<&str> = self.filename.split(['/', '\\']).collect();
if let Some(idx) = parts.iter().position(|&p| p == "node_modules") {
if let Some(lib) = parts.get(idx + 1) {
return format!("{lib}_{base}");
}
}
base
}
fn next_name(&mut self, func_name: Option<&str>) -> (String, String) {
let source = self.source_name();
let suffix = format!("{}{}", source, self.worklet_number);
self.worklet_number += 1;
let worklet_name = func_name
.filter(|n| !n.is_empty())
.map(|n| sanitize_ident(&format!("{n}_{suffix}")))
.unwrap_or_else(|| sanitize_ident(&suffix));
let react_name = func_name
.filter(|n| !n.is_empty())
.map(sanitize_ident)
.unwrap_or_else(|| sanitize_ident(&suffix));
(worklet_name, react_name)
}
fn has_worklet_directive(stmts: &[Stmt]) -> bool {
stmts
.iter()
.any(|s| str_stmt_value(s) == Some(WORKLET_DIRECTIVE))
}
fn strip_worklet_directives(stmts: &mut Vec<Stmt>) {
stmts.retain(|s| match str_stmt_value(s) {
Some(v) => !matches!(
v,
WORKLET_DIRECTIVE | "no-worklet-closure" | "limit-init-data-hoisting"
),
None => true,
});
}
fn is_already_workletized(stmts: &[Stmt]) -> bool {
stmts.iter().any(|s| {
if let Stmt::Expr(ExprStmt { expr, .. }) = s {
if let Expr::Assign(AssignExpr {
left: AssignTarget::Simple(SimpleAssignTarget::Member(me)),
..
}) = expr.as_ref()
{
if let MemberProp::Ident(id) = &me.prop {
return id.sym.as_ref() == "__workletHash";
}
}
}
false
})
}
fn make_factory_call(
&mut self,
func_name: Option<&str>,
params: Vec<Param>,
body: BlockStmt,
mut closure_vars: Vec<Ident>,
is_generator: bool,
is_async: bool,
) -> Expr {
let (worklet_name, react_name) = self.next_name(func_name);
let mut init_body = body.clone();
if let Some(orig) = func_name.filter(|n| !n.is_empty()) {
if orig != worklet_name && rename_free_refs(&mut init_body, orig, &worklet_name) {
init_body.stmts.insert(0, const_recur_decl(&worklet_name));
closure_vars.retain(|v| v.sym.as_ref() != orig);
}
}
substitute_worklet_class_news(&mut init_body, &mut closure_vars);
let location_str = if self.is_release {
String::new()
} else {
self.location_path()
};
let (code_str, real_source_map) = build_worklet_code_and_map(
&worklet_name,
¶ms,
&init_body,
&closure_vars,
is_generator,
is_async,
self.source_map.as_ref(),
&location_str,
);
let hash = worklet_hash(&code_str);
let init_id = format!("_worklet_{hash}_init_data");
let should_include_init_data =
!self.options.omit_native_only_data && !self.options.bundle_mode;
let should_include_stack_details = !self.is_release && !self.options.bundle_mode;
let source_map_str =
if !should_include_init_data || self.is_release || self.options.disable_source_maps {
None
} else {
real_source_map
};
if should_include_init_data {
self.pending_prepends.push(make_init_data_decl(
&init_id,
&code_str,
&location_str,
source_map_str.as_deref(),
));
}
let mut stmts: Vec<Stmt> = vec![];
if should_include_stack_details {
stmts.push(make_stack_details_decl());
}
stmts.push(const_named_fn(
&react_name,
params,
body,
is_generator,
is_async,
));
let closure_props = closure_vars
.iter()
.map(|v| shorthand_prop(v.sym.as_ref()))
.collect();
stmts.push(assign_member(
&react_name,
"__closure",
Expr::Object(ObjectLit {
span: DUMMY_SP,
props: closure_props,
}),
));
stmts.push(assign_member(
&react_name,
"__workletHash",
num_lit(hash as f64),
));
if !self.is_release {
stmts.push(assign_member(
&react_name,
"__pluginVersion",
str_lit(self.plugin_version()),
));
}
if should_include_init_data {
stmts.push(assign_member(
&react_name,
"__initData",
ident_expr(&init_id),
));
}
if should_include_stack_details {
stmts.push(assign_member(
&react_name,
"__stackDetails",
ident_expr("_e"),
));
}
stmts.push(Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(Box::new(ident_expr(&react_name))),
}));
let factory_fn = FnExpr {
ident: Some(id(&format!("{worklet_name}Factory"))),
function: Box::new(Function {
params: vec![factory_param(
&closure_vars,
should_include_init_data.then_some(init_id.as_str()),
)],
decorators: vec![],
span: DUMMY_SP,
ctxt: Default::default(),
body: Some(BlockStmt {
span: DUMMY_SP,
stmts,
ctxt: Default::default(),
}),
is_generator: false,
is_async: false,
type_params: None,
return_type: None,
}),
};
let call_obj = factory_call_obj(
&closure_vars,
should_include_init_data.then_some(init_id.as_str()),
);
Expr::Fn(factory_fn)
.wrap_with_paren()
.as_call(DUMMY_SP, vec![call_obj.as_arg()])
}
fn try_workletize_fn(&mut self, name_hint: Option<&str>, fn_expr: &mut FnExpr) -> Option<Expr> {
let body = fn_expr.function.body.as_mut()?;
if Self::is_already_workletized(&body.stmts) || !Self::has_worklet_directive(&body.stmts) {
return None;
}
Self::strip_worklet_directives(&mut body.stmts);
let params = fn_expr.function.params.clone();
let body_c = body.clone();
let cv = collect_closure_vars(¶ms, &body_c, &self.closure_ctx());
let name = name_hint
.map(String::from)
.or_else(|| fn_expr.ident.as_ref().map(|i| i.sym.to_string()));
Some(self.make_factory_call(
name.as_deref(),
params,
body_c,
cv,
fn_expr.function.is_generator,
fn_expr.function.is_async,
))
}
fn try_workletize_arrow(
&mut self,
name_hint: Option<&str>,
arrow: &mut ArrowExpr,
) -> Option<Expr> {
let body = match arrow.body.as_mut() {
BlockStmtOrExpr::BlockStmt(b) => b,
_ => return None,
};
if Self::is_already_workletized(&body.stmts) || !Self::has_worklet_directive(&body.stmts) {
return None;
}
Self::strip_worklet_directives(&mut body.stmts);
let pats = arrow.params.clone();
let params: Vec<Param> = pats
.iter()
.map(|p| Param {
span: DUMMY_SP,
decorators: vec![],
pat: p.clone(),
})
.collect();
let body_c = body.clone();
let cv = collect_closure_vars_arrow(
&pats,
&BlockStmtOrExpr::BlockStmt(body_c.clone()),
&self.closure_ctx(),
);
Some(self.make_factory_call(name_hint, params, body_c, cv, false, arrow.is_async))
}
fn force_workletize(&mut self, expr: &mut Expr, accept_fn: bool, accept_obj: bool) -> bool {
match expr {
Expr::Fn(fn_expr) if accept_fn => {
let body = match fn_expr.function.body.as_mut() {
Some(b) => b,
None => return false,
};
if Self::is_already_workletized(&body.stmts) {
return false;
}
Self::strip_worklet_directives(&mut body.stmts);
let params = fn_expr.function.params.clone();
let body_c = body.clone();
let cv = collect_closure_vars(¶ms, &body_c, &self.closure_ctx());
let name = fn_expr.ident.as_ref().map(|i| i.sym.to_string());
let new = self.make_factory_call(
name.as_deref(),
params,
body_c,
cv,
fn_expr.function.is_generator,
fn_expr.function.is_async,
);
*expr = new;
true
}
Expr::Arrow(arrow) if accept_fn => {
ensure_block_body(arrow);
let body = match arrow.body.as_mut() {
BlockStmtOrExpr::BlockStmt(b) => b,
_ => return false,
};
if Self::is_already_workletized(&body.stmts) {
return false;
}
Self::strip_worklet_directives(&mut body.stmts);
let pats = arrow.params.clone();
let params: Vec<Param> = pats
.iter()
.map(|p| Param {
span: DUMMY_SP,
decorators: vec![],
pat: p.clone(),
})
.collect();
let body_c = body.clone();
let cv = collect_closure_vars_arrow(
&pats,
&BlockStmtOrExpr::BlockStmt(body_c.clone()),
&self.closure_ctx(),
);
let new = self.make_factory_call(None, params, body_c, cv, false, arrow.is_async);
*expr = new;
true
}
Expr::Object(obj) if accept_obj => self.force_workletize_obj(obj),
Expr::Seq(seq) => {
if let Some(last) = seq.exprs.last_mut() {
self.force_workletize(last, accept_fn, accept_obj)
} else {
false
}
}
Expr::Paren(paren) => self.force_workletize(&mut paren.expr, accept_fn, accept_obj),
_ => false,
}
}
fn try_workletize_fn_decl(&mut self, fn_decl: &mut FnDecl) -> Option<VarDecl> {
fn_decl.function.visit_mut_with(self);
let body = fn_decl.function.body.as_mut()?;
if Self::is_already_workletized(&body.stmts) || !Self::has_worklet_directive(&body.stmts) {
return None;
}
Self::strip_worklet_directives(&mut body.stmts);
let name = fn_decl.ident.sym.to_string();
let params = fn_decl.function.params.clone();
let body_c = body.clone();
let cv = collect_closure_vars(¶ms, &body_c, &self.closure_ctx());
let fc = self.make_factory_call(
Some(&name),
params,
body_c,
cv,
fn_decl.function.is_generator,
fn_decl.function.is_async,
);
Some(VarDecl {
span: DUMMY_SP,
ctxt: Default::default(),
kind: VarDeclKind::Const,
declare: false,
decls: vec![VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(BindingIdent {
id: fn_decl.ident.clone(),
type_ann: None,
}),
init: Some(Box::new(fc)),
definite: false,
}],
})
}
fn workletize_class_body(&mut self, class: &mut Class) {
let marker_idx = class.body.iter().position(is_worklet_class_marker);
if let Some(idx) = marker_idx {
if self.options.disable_worklet_classes || self.options.bundle_mode {
return;
}
class.body.remove(idx);
for m in &mut class.body {
match m {
ClassMember::Method(method) => {
if let Some(body) = method.function.body.as_mut() {
push_dir(body);
}
}
ClassMember::Constructor(c) => {
if let Some(body) = c.body.as_mut() {
push_dir(body);
}
}
ClassMember::ClassProp(p) => {
if let Some(value) = p.value.as_mut() {
add_worklet_dir_expr(value);
}
}
_ => {}
}
}
}
let members = std::mem::take(&mut class.body);
let mut new_members = Vec::with_capacity(members.len());
for m in members {
match m {
ClassMember::Method(method) => {
new_members.push(self.workletize_class_method(method));
}
ClassMember::Constructor(ctor) => {
new_members.push(self.workletize_class_constructor(ctor));
}
other => new_members.push(other),
}
}
class.body = new_members;
}
fn wrap_marked_class_in_factory_items(
&mut self,
item: ModuleItem,
class_ident: Ident,
) -> Vec<ModuleItem> {
let Some((class_node, export_kind)) = take_class_expr_from_item(item) else {
unreachable!("wrap_marked_class_in_factory_items called on non-class item");
};
let factory_var_decl = self.build_class_factory_var_decl(&class_ident, class_node);
let invocation_var_decl = build_factory_invocation_var_decl(&class_ident);
let mut out = Vec::with_capacity(3);
out.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(
factory_var_decl,
)))));
match export_kind {
ClassExportKind::None => {
out.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(
invocation_var_decl,
)))));
}
ClassExportKind::Named(span) => {
out.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
span,
decl: Decl::Var(Box::new(invocation_var_decl)),
})));
}
ClassExportKind::Default(span) => {
out.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(
invocation_var_decl,
)))));
out.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(
ExportDefaultExpr {
span,
expr: Box::new(Expr::Ident(class_ident)),
},
)));
}
}
out
}
fn wrap_marked_class_in_factory_stmts(&mut self, stmt: Stmt, class_ident: Ident) -> Vec<Stmt> {
let Some(class_node) = take_class_expr_from_stmt(stmt) else {
unreachable!("wrap_marked_class_in_factory_stmts called on non-class stmt");
};
let factory_var_decl = self.build_class_factory_var_decl(&class_ident, class_node);
let invocation_var_decl = build_factory_invocation_var_decl(&class_ident);
vec![
Stmt::Decl(Decl::Var(Box::new(factory_var_decl))),
Stmt::Decl(Decl::Var(Box::new(invocation_var_decl))),
]
}
fn build_class_factory_var_decl(
&mut self,
class_ident: &Ident,
class_node: Box<Class>,
) -> VarDecl {
let factory_name = format!(
"{}{}",
class_ident.sym.as_ref(),
WORKLET_CLASS_FACTORY_SUFFIX
);
let factory_ident = Ident::from(Atom::from(factory_name.as_str()));
let factory_body = build_class_factory_body(class_ident, &factory_ident, class_node);
let mut factory_fn_decl = FnDecl {
ident: factory_ident,
declare: false,
function: Box::new(Function {
params: vec![],
decorators: vec![],
span: DUMMY_SP,
ctxt: Default::default(),
body: Some(factory_body),
is_generator: false,
is_async: false,
type_params: None,
return_type: None,
}),
};
self.try_workletize_fn_decl(&mut factory_fn_decl).expect(
"factory FnDecl has a `'worklet'` directive — try_workletize_fn_decl must succeed",
)
}
fn workletize_class_method(&mut self, mut method: ClassMethod) -> ClassMember {
let body_has_directive = method.function.body.as_ref().is_some_and(|b| {
!Self::is_already_workletized(&b.stmts) && Self::has_worklet_directive(&b.stmts)
});
if !body_has_directive {
return ClassMember::Method(method);
}
if let Some(body) = method.function.body.as_mut() {
Self::strip_worklet_directives(&mut body.stmts);
}
let Some(body) = method.function.body.clone() else {
return ClassMember::Method(method);
};
let params = method.function.params.clone();
let cv = collect_closure_vars(¶ms, &body, &self.closure_ctx());
let name = prop_name_str(&method.key);
let factory_call = self.make_factory_call(
name,
params,
body,
cv,
method.function.is_generator,
method.function.is_async,
);
match method.kind {
MethodKind::Method => ClassMember::ClassProp(ClassProp {
span: method.span,
key: method.key,
value: Some(Box::new(factory_call)),
type_ann: None,
is_static: method.is_static,
decorators: vec![],
accessibility: method.accessibility,
is_abstract: method.is_abstract,
is_optional: method.is_optional,
is_override: method.is_override,
readonly: false,
declare: false,
definite: false,
}),
MethodKind::Getter | MethodKind::Setter => {
let _ = factory_call;
ClassMember::Method(method)
}
}
}
fn workletize_class_constructor(&mut self, mut ctor: Constructor) -> ClassMember {
let body_has_directive = ctor.body.as_ref().is_some_and(|b| {
!Self::is_already_workletized(&b.stmts) && Self::has_worklet_directive(&b.stmts)
});
if !body_has_directive {
return ClassMember::Constructor(ctor);
}
if let Some(body) = ctor.body.as_mut() {
Self::strip_worklet_directives(&mut body.stmts);
}
let Some(body) = ctor.body.clone() else {
return ClassMember::Constructor(ctor);
};
let params: Vec<Param> = ctor
.params
.iter()
.filter_map(|p| match p {
ParamOrTsParamProp::Param(p) => Some(p.clone()),
ParamOrTsParamProp::TsParamProp(_) => None,
})
.collect();
let cv = collect_closure_vars(¶ms, &body, &self.closure_ctx());
let _ = self.make_factory_call(Some("constructor"), params, body, cv, false, false);
ClassMember::Constructor(ctor)
}
fn process_context_object(&mut self, obj: &mut ObjectLit) -> bool {
let Some(marker_idx) = obj.props.iter().position(is_context_object_marker) else {
return false;
};
obj.props.remove(marker_idx);
let cloned = obj.clone();
let factory_body = BlockStmt {
span: DUMMY_SP,
stmts: vec![
Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(str_lit(WORKLET_DIRECTIVE)),
}),
Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(Box::new(Expr::Object(cloned))),
}),
],
ctxt: Default::default(),
};
let factory_method = MethodProp {
key: PropName::Ident(IdentName::new(CONTEXT_OBJECT_FACTORY.into(), DUMMY_SP)),
function: Box::new(Function {
params: vec![],
decorators: vec![],
span: DUMMY_SP,
ctxt: Default::default(),
body: Some(factory_body),
is_generator: false,
is_async: false,
type_params: None,
return_type: None,
}),
};
obj.props
.push(PropOrSpread::Prop(Box::new(Prop::Method(factory_method))));
true
}
fn workletize_directive_methods(&mut self, obj: &mut ObjectLit) {
let props = std::mem::take(&mut obj.props);
let mut new_props = Vec::with_capacity(props.len());
for mut p in props {
if let PropOrSpread::Prop(prop_box) = &mut p {
if let Prop::Method(m) = prop_box.as_mut() {
if let Some(body) = m.function.body.as_mut() {
if !Self::is_already_workletized(&body.stmts)
&& Self::has_worklet_directive(&body.stmts)
{
Self::strip_worklet_directives(&mut body.stmts);
let params = m.function.params.clone();
let body_c = body.clone();
let cv = collect_closure_vars(¶ms, &body_c, &self.closure_ctx());
let name = prop_name_str(&m.key);
let fc = self.make_factory_call(
name,
params,
body_c,
cv,
m.function.is_generator,
m.function.is_async,
);
new_props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
KeyValueProp {
key: m.key.clone(),
value: Box::new(fc),
},
))));
continue;
}
}
}
}
new_props.push(p);
}
obj.props = new_props;
}
#[allow(clippy::collapsible_match)]
fn force_workletize_obj(&mut self, obj: &mut ObjectLit) -> bool {
let mut any = false;
let props = std::mem::take(&mut obj.props);
let mut new_props = Vec::with_capacity(props.len());
for mut p in props {
if let PropOrSpread::Prop(prop_box) = &mut p {
match prop_box.as_mut() {
Prop::Method(m) => {
if let Some(body) = m.function.body.clone() {
let params = m.function.params.clone();
let cv = collect_closure_vars(¶ms, &body, &self.closure_ctx());
let name = prop_name_str(&m.key);
let fc = self.make_factory_call(
name,
params,
body,
cv,
m.function.is_generator,
m.function.is_async,
);
new_props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
KeyValueProp {
key: m.key.clone(),
value: Box::new(fc),
},
))));
any = true;
continue;
}
}
Prop::KeyValue(kv) => {
if self.force_workletize(&mut kv.value, true, false) {
any = true;
}
}
_ => {}
}
}
new_props.push(p);
}
obj.props = new_props;
any
}
fn is_gesture_callback(callee: &Expr) -> bool {
if let Expr::Member(me) = callee {
if let MemberProp::Ident(prop) = &me.prop {
if GESTURE_BUILDER_METHODS.contains(&prop.sym.as_ref()) {
return contains_gesture_obj(&me.obj);
}
}
}
false
}
fn is_layout_anim_callback(callee: &Expr) -> bool {
if let Expr::Member(me) = callee {
if let MemberProp::Ident(prop) = &me.prop {
if LAYOUT_ANIM_CALLBACKS.contains(&prop.sym.as_ref()) {
return is_layout_anim_chain(&me.obj);
}
}
}
false
}
fn resolve_referenced_worklets(&self, items: &mut [ModuleItem]) {
for item in items.iter_mut() {
resolve_refs_in_nested_scopes_item(item);
}
let mut refs: indexmap::IndexMap<Atom, bool> = indexmap::IndexMap::new();
for item in items.iter() {
match item {
ModuleItem::Stmt(s) => collect_ref_names_stmt(s, &mut refs),
ModuleItem::ModuleDecl(m) => collect_ref_names_moduledecl(m, &mut refs),
}
}
if refs.is_empty() {
return;
}
for (name, accept_obj) in refs {
tag_best_binding_in_items(items, name.as_ref(), accept_obj);
}
}
fn resolve_referenced_worklets_script(&self, stmts: &mut [Stmt]) {
for stmt in stmts.iter_mut() {
resolve_refs_in_nested_scopes_stmt(stmt);
}
let mut refs: indexmap::IndexMap<Atom, bool> = indexmap::IndexMap::new();
for stmt in stmts.iter() {
collect_ref_names_stmt(stmt, &mut refs);
}
if refs.is_empty() {
return;
}
for (name, accept_obj) in refs {
tag_best_binding_in_stmts(stmts, name.as_ref(), accept_obj);
}
}
fn handle_file_worklet(&self, module: &mut Module) {
let has = module.body.iter().any(|item| {
if let ModuleItem::Stmt(Stmt::Expr(ExprStmt { expr, .. })) = item {
if let Expr::Lit(Lit::Str(s)) = expr.as_ref() {
return s.value == *WORKLET_DIRECTIVE;
}
}
false
});
if !has {
return;
}
module.body.retain(|item| {
if let ModuleItem::Stmt(Stmt::Expr(ExprStmt { expr, .. })) = item {
if let Expr::Lit(Lit::Str(s)) = expr.as_ref() {
return s.value != *WORKLET_DIRECTIVE;
}
}
true
});
let mut non_exports: Vec<ModuleItem> = Vec::with_capacity(module.body.len());
let mut cjs_exports: Vec<ModuleItem> = Vec::new();
for item in std::mem::take(&mut module.body) {
if is_cjs_export(&item) {
cjs_exports.push(item);
} else {
non_exports.push(item);
}
}
non_exports.extend(cjs_exports);
module.body = non_exports;
for item in &mut module.body {
add_worklet_dir_item(item);
}
}
fn handle_file_worklet_script(&self, script: &mut Script) {
let has = script.body.iter().any(|s| {
if let Stmt::Expr(ExprStmt { expr, .. }) = s {
if let Expr::Lit(Lit::Str(s)) = expr.as_ref() {
return s.value == *WORKLET_DIRECTIVE;
}
}
false
});
if !has {
return;
}
script.body.retain(|s| {
if let Stmt::Expr(ExprStmt { expr, .. }) = s {
if let Expr::Lit(Lit::Str(s)) = expr.as_ref() {
return s.value != *WORKLET_DIRECTIVE;
}
}
true
});
let mut non_exports: Vec<Stmt> = Vec::with_capacity(script.body.len());
let mut cjs_exports: Vec<Stmt> = Vec::new();
for stmt in std::mem::take(&mut script.body) {
if is_cjs_export_stmt(&stmt) {
cjs_exports.push(stmt);
} else {
non_exports.push(stmt);
}
}
non_exports.extend(cjs_exports);
script.body = non_exports;
for stmt in &mut script.body {
add_worklet_dir_stmt(stmt);
}
}
fn maybe_warn_inline_styles(&self, attr: &mut JSXAttr) {
if self.is_release || self.options.disable_inline_styles_warning {
return;
}
if !matches!(&attr.name, JSXAttrName::Ident(id) if id.sym.as_ref() == "style") {
return;
}
if let Some(JSXAttrValue::JSXExprContainer(c)) = &mut attr.value {
if let JSXExpr::Expr(e) = &mut c.expr {
match e.as_mut() {
Expr::Array(arr) => {
for ExprOrSpread { expr, .. } in arr.elems.iter_mut().flatten() {
if let Expr::Object(obj) = expr.as_mut() {
warn_obj(obj);
}
}
}
Expr::Object(obj) => warn_obj(obj),
_ => {}
}
}
}
}
}
impl VisitMut for WorkletsVisitor {
fn visit_mut_module(&mut self, module: &mut Module) {
if self.options.bundle_mode {
self.report_unsupported_bundle_mode(module.span);
return;
}
if self.skip_file {
return;
}
self.file_bindings = crate::closure::collect_file_bindings_module(module);
self.handle_file_worklet(module);
self.resolve_referenced_worklets(&mut module.body);
let old = std::mem::take(&mut module.body);
let mut out: Vec<ModuleItem> = Vec::with_capacity(old.len());
for mut item in old {
let marked_class_ident = detect_marked_class_decl_ident(&item, &self.options);
item.visit_mut_with(self);
for p in self.pending_prepends.drain(..) {
out.push(ModuleItem::Stmt(p));
}
if let Some(class_ident) = marked_class_ident {
let wrapped = self.wrap_marked_class_in_factory_items(item, class_ident);
for p in self.pending_prepends.drain(..) {
out.push(ModuleItem::Stmt(p));
}
out.extend(wrapped);
} else {
out.push(item);
}
}
module.body = out;
}
fn visit_mut_script(&mut self, script: &mut Script) {
if self.options.bundle_mode {
self.report_unsupported_bundle_mode(script.span);
return;
}
if self.skip_file {
return;
}
self.file_bindings = crate::closure::collect_file_bindings_script(script);
self.handle_file_worklet_script(script);
self.resolve_referenced_worklets_script(&mut script.body);
let old = std::mem::take(&mut script.body);
let mut out: Vec<Stmt> = Vec::with_capacity(old.len());
for mut s in old {
let marked_class_ident = detect_marked_class_decl_ident_stmt(&s, &self.options);
s.visit_mut_with(self);
for p in self.pending_prepends.drain(..) {
out.push(p);
}
if let Some(class_ident) = marked_class_ident {
let wrapped = self.wrap_marked_class_in_factory_stmts(s, class_ident);
for p in self.pending_prepends.drain(..) {
out.push(p);
}
out.extend(wrapped);
} else {
out.push(s);
}
}
script.body = out;
}
fn visit_mut_block_stmt(&mut self, block: &mut BlockStmt) {
block.visit_mut_children_with(self);
}
fn visit_mut_expr(&mut self, expr: &mut Expr) {
expr.visit_mut_children_with(self);
match expr {
Expr::Fn(fn_expr) => {
if let Some(new) = self.try_workletize_fn(None, fn_expr) {
*expr = new;
}
}
Expr::Arrow(arrow) => {
if let Some(new) = self.try_workletize_arrow(None, arrow) {
*expr = new;
}
}
Expr::Object(obj) => {
self.process_context_object(obj);
self.workletize_directive_methods(obj);
}
Expr::Call(call) => {
if self.options.substitute_web_platform_checks {
let _ = crate::web::substitute_web_call_expression(call);
}
let callee_name = callee_ident(&call.callee);
if let Some(name) = callee_name {
if let Some((_, idxs)) = function_hooks().iter().find(|(h, _)| *h == name) {
let accept_obj = is_object_hook(name);
let n = call.args.len();
for &idx in *idxs {
if idx < n {
self.force_workletize(&mut call.args[idx].expr, true, accept_obj);
}
}
return;
}
}
if let Callee::Expr(ce) = &call.callee.clone() {
if Self::is_gesture_callback(ce) {
for arg in &mut call.args {
self.force_workletize(&mut arg.expr, true, true);
}
return;
}
if Self::is_layout_anim_callback(ce) {
for arg in &mut call.args {
self.force_workletize(&mut arg.expr, true, false);
}
}
}
}
_ => {}
}
}
fn visit_mut_stmt(&mut self, stmt: &mut Stmt) {
if let Stmt::Decl(Decl::Fn(fn_decl)) = stmt {
if let Some(var_decl) = self.try_workletize_fn_decl(fn_decl) {
*stmt = Stmt::Decl(Decl::Var(Box::new(var_decl)));
}
} else {
stmt.visit_mut_children_with(self);
}
}
fn visit_mut_class(&mut self, class: &mut Class) {
class.visit_mut_children_with(self);
self.workletize_class_body(class);
}
fn visit_mut_module_item(&mut self, item: &mut ModuleItem) {
match item {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) => match &mut e.decl {
Decl::Fn(fn_decl) => {
if let Some(var_decl) = self.try_workletize_fn_decl(fn_decl) {
e.decl = Decl::Var(Box::new(var_decl));
}
}
Decl::Var(v) => {
v.visit_mut_with(self);
}
_ => e.decl.visit_mut_with(self),
},
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(e)) => {
if let DefaultDecl::Fn(fn_expr) = &mut e.decl {
if let Some(body) = fn_expr.function.body.as_mut() {
if !Self::is_already_workletized(&body.stmts)
&& Self::has_worklet_directive(&body.stmts)
{
Self::strip_worklet_directives(&mut body.stmts);
let params = fn_expr.function.params.clone();
let body_c = body.clone();
let cv = collect_closure_vars(¶ms, &body_c, &self.closure_ctx());
let name = fn_expr.ident.as_ref().map(|i| i.sym.to_string());
let fc = self.make_factory_call(
name.as_deref(),
params,
body_c,
cv,
fn_expr.function.is_generator,
fn_expr.function.is_async,
);
*item = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(
ExportDefaultExpr {
span: DUMMY_SP,
expr: Box::new(fc),
},
));
return;
}
}
}
item.visit_mut_children_with(self);
}
_ => item.visit_mut_children_with(self),
}
}
fn visit_mut_var_declarator(&mut self, decl: &mut VarDeclarator) {
decl.visit_mut_children_with(self);
if let Some(init) = &mut decl.init {
let hint = pat_ident(&decl.name);
match init.as_mut() {
Expr::Fn(fn_expr) => {
if let Some(new) = self.try_workletize_fn(hint, fn_expr) {
**init = new;
}
}
Expr::Arrow(arrow) => {
if let Some(new) = self.try_workletize_arrow(hint, arrow) {
**init = new;
}
}
_ => {}
}
}
}
fn visit_mut_jsx_attr(&mut self, attr: &mut JSXAttr) {
attr.visit_mut_children_with(self);
self.maybe_warn_inline_styles(attr);
}
}
fn is_cjs_export(item: &ModuleItem) -> bool {
let ModuleItem::Stmt(stmt) = item else {
return false;
};
is_cjs_export_stmt(stmt)
}
fn is_cjs_export_stmt(stmt: &Stmt) -> bool {
let Stmt::Expr(ExprStmt { expr, .. }) = stmt else {
return false;
};
match expr.as_ref() {
Expr::Assign(AssignExpr {
left,
op: AssignOp::Assign,
..
}) => {
let AssignTarget::Simple(SimpleAssignTarget::Member(me)) = left else {
return false;
};
member_root_is_cjs(&me.obj)
}
Expr::Call(CallExpr {
callee: Callee::Expr(ce),
args,
..
}) => {
let Expr::Member(me) = ce.as_ref() else {
return false;
};
let Expr::Ident(obj) = me.obj.as_ref() else {
return false;
};
if obj.sym.as_ref() != "Object" {
return false;
}
let MemberProp::Ident(prop) = &me.prop else {
return false;
};
if prop.sym.as_ref() != "defineProperty" {
return false;
}
matches!(
args.first().map(|a| a.expr.as_ref()),
Some(Expr::Ident(id)) if id.sym.as_ref() == "exports"
)
}
_ => false,
}
}
fn member_root_is_cjs(expr: &Expr) -> bool {
match expr {
Expr::Ident(id) => matches!(id.sym.as_ref(), "module" | "exports"),
Expr::Member(me) => member_root_is_cjs(&me.obj),
_ => false,
}
}
fn resolve_refs_in_nested_scopes_item(item: &mut ModuleItem) {
match item {
ModuleItem::Stmt(s) => resolve_refs_in_nested_scopes_stmt(s),
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) => {
resolve_refs_in_nested_scopes_decl(&mut e.decl);
}
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(e)) => match &mut e.decl {
DefaultDecl::Fn(fn_expr) => {
if let Some(body) = fn_expr.function.body.as_mut() {
resolve_refs_in_block(body);
}
}
DefaultDecl::Class(class_expr) => {
resolve_refs_in_class(&mut class_expr.class);
}
_ => {}
},
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(e)) => {
resolve_refs_in_nested_scopes_expr(&mut e.expr);
}
_ => {}
}
}
fn resolve_refs_in_nested_scopes_stmt(stmt: &mut Stmt) {
match stmt {
Stmt::Decl(d) => resolve_refs_in_nested_scopes_decl(d),
Stmt::Block(b) => resolve_refs_in_block(b),
Stmt::If(i) => {
resolve_refs_in_nested_scopes_stmt(&mut i.cons);
if let Some(alt) = i.alt.as_mut() {
resolve_refs_in_nested_scopes_stmt(alt);
}
}
Stmt::While(w) => resolve_refs_in_nested_scopes_stmt(&mut w.body),
Stmt::DoWhile(w) => resolve_refs_in_nested_scopes_stmt(&mut w.body),
Stmt::For(f) => resolve_refs_in_nested_scopes_stmt(&mut f.body),
Stmt::ForIn(f) => resolve_refs_in_nested_scopes_stmt(&mut f.body),
Stmt::ForOf(f) => resolve_refs_in_nested_scopes_stmt(&mut f.body),
Stmt::Try(t) => {
resolve_refs_in_block(&mut t.block);
if let Some(h) = t.handler.as_mut() {
resolve_refs_in_block(&mut h.body);
}
if let Some(f) = t.finalizer.as_mut() {
resolve_refs_in_block(f);
}
}
Stmt::Expr(e) => resolve_refs_in_nested_scopes_expr(&mut e.expr),
_ => {}
}
}
fn resolve_refs_in_nested_scopes_decl(decl: &mut Decl) {
match decl {
Decl::Fn(fd) => {
if let Some(body) = fd.function.body.as_mut() {
resolve_refs_in_block(body);
}
}
Decl::Var(v) => {
for d in &mut v.decls {
if let Some(init) = d.init.as_mut() {
resolve_refs_in_nested_scopes_expr(init);
}
}
}
Decl::Class(c) => resolve_refs_in_class(&mut c.class),
_ => {}
}
}
fn resolve_refs_in_nested_scopes_expr(expr: &mut Expr) {
match expr {
Expr::Fn(fn_expr) => {
if let Some(body) = fn_expr.function.body.as_mut() {
resolve_refs_in_block(body);
}
}
Expr::Arrow(arrow) => {
if let BlockStmtOrExpr::BlockStmt(b) = arrow.body.as_mut() {
resolve_refs_in_block(b);
}
}
Expr::Class(ce) => resolve_refs_in_class(&mut ce.class),
_ => {}
}
}
fn resolve_refs_in_class(class: &mut Class) {
for m in &mut class.body {
match m {
ClassMember::Method(method) => {
if let Some(body) = method.function.body.as_mut() {
resolve_refs_in_block(body);
}
}
ClassMember::Constructor(c) => {
if let Some(body) = c.body.as_mut() {
resolve_refs_in_block(body);
}
}
ClassMember::ClassProp(p) => {
if let Some(value) = p.value.as_mut() {
resolve_refs_in_nested_scopes_expr(value);
}
}
_ => {}
}
}
}
fn resolve_refs_in_block(block: &mut BlockStmt) {
for stmt in &mut block.stmts {
resolve_refs_in_nested_scopes_stmt(stmt);
}
let mut refs: indexmap::IndexMap<Atom, bool> = indexmap::IndexMap::new();
for stmt in block.stmts.iter() {
collect_ref_names_stmt(stmt, &mut refs);
}
if refs.is_empty() {
return;
}
for (name, accept_obj) in refs {
if has_binding_for(&block.stmts, name.as_ref()) {
tag_best_binding_in_stmts(&mut block.stmts, name.as_ref(), accept_obj);
}
}
}
fn has_binding_for(stmts: &[Stmt], name: &str) -> bool {
for stmt in stmts {
if let Stmt::Decl(decl) = stmt {
match decl {
Decl::Fn(fd) if fd.ident.sym.as_ref() == name => return true,
Decl::Var(v) => {
for d in &v.decls {
if let Pat::Ident(bi) = &d.name {
if bi.id.sym.as_ref() == name {
return true;
}
}
}
}
_ => {}
}
}
let mut finder = AssignToNameFinder {
name,
count: 0,
last_seq_idx: None,
};
stmt.visit_with(&mut finder);
if finder.last_seq_idx.is_some() {
return true;
}
}
false
}
fn collect_ref_names_stmt(stmt: &Stmt, out: &mut indexmap::IndexMap<Atom, bool>) {
stmt.visit_with(&mut ReferenceCollector { out });
}
fn collect_ref_names_moduledecl(m: &ModuleDecl, out: &mut indexmap::IndexMap<Atom, bool>) {
m.visit_with(&mut ReferenceCollector { out });
}
struct ReferenceCollector<'a> {
out: &'a mut indexmap::IndexMap<Atom, bool>,
}
impl ReferenceCollector<'_> {
fn record_hook_arg(&mut self, expr: &Expr, accept_obj: bool) {
match expr {
Expr::Ident(id) => {
self.out
.entry(id.sym.clone())
.and_modify(|v| *v |= accept_obj)
.or_insert(accept_obj);
}
Expr::Paren(p) => self.record_hook_arg(&p.expr, accept_obj),
Expr::Seq(s) => {
if let Some(last) = s.exprs.last() {
self.record_hook_arg(last, accept_obj);
}
}
Expr::Object(obj) if accept_obj => {
for prop in &obj.props {
if let PropOrSpread::Prop(p) = prop {
match p.as_ref() {
Prop::Shorthand(id) => {
self.out.entry(id.sym.clone()).or_insert(false);
}
Prop::KeyValue(kv) => {
if let Expr::Ident(id) = kv.value.as_ref() {
self.out.entry(id.sym.clone()).or_insert(false);
}
}
_ => {}
}
}
}
}
_ => {}
}
}
}
impl Visit for ReferenceCollector<'_> {
fn visit_call_expr(&mut self, call: &CallExpr) {
call.visit_children_with(self);
let Callee::Expr(ce) = &call.callee else {
return;
};
let callee_atom: Option<&Atom> = match ce.as_ref() {
Expr::Ident(id) => Some(&id.sym),
Expr::Member(me) => match &me.prop {
MemberProp::Ident(p) => Some(&p.sym),
_ => None,
},
_ => None,
};
let Some(name) = callee_atom else {
return;
};
if let Some((_, idxs)) = function_hooks().iter().find(|(h, _)| *h == name.as_ref()) {
let accept_obj = is_object_hook(name);
for &idx in *idxs {
let Some(arg) = call.args.get(idx) else {
continue;
};
self.record_hook_arg(&arg.expr, accept_obj);
}
return;
}
if let Expr::Member(me) = ce.as_ref() {
if let MemberProp::Ident(prop) = &me.prop {
if GESTURE_BUILDER_METHODS.contains(&prop.sym.as_ref())
&& contains_gesture_obj(&me.obj)
{
for arg in &call.args {
self.record_hook_arg(&arg.expr, false);
}
}
}
}
}
}
#[derive(Debug)]
enum RefTarget {
FnDecl(usize),
Assign {
stmt_idx: usize,
assign_idx_in_seq: usize,
},
VarDecl {
stmt_idx: usize,
declarator_idx: usize,
},
}
fn find_best_binding_items(items: &[ModuleItem], name: &str) -> Option<RefTarget> {
let stmts: Vec<Option<&Stmt>> = items
.iter()
.map(|item| match item {
ModuleItem::Stmt(s) => Some(s),
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) => match &e.decl {
Decl::Fn(_) | Decl::Var(_) => None, _ => None,
},
_ => None,
})
.collect();
find_best_binding_impl(&stmts, name, |idx| item_as_decl(&items[idx]))
}
fn find_best_binding_stmts(stmts: &[Stmt], name: &str) -> Option<RefTarget> {
let stmts_opt: Vec<Option<&Stmt>> = stmts.iter().map(Some).collect();
find_best_binding_impl(&stmts_opt, name, |idx| stmt_as_decl(&stmts[idx]))
}
fn item_as_decl(item: &ModuleItem) -> Option<&Decl> {
match item {
ModuleItem::Stmt(Stmt::Decl(d)) => Some(d),
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) => Some(&e.decl),
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(_)) => None,
_ => None,
}
}
fn stmt_as_decl(stmt: &Stmt) -> Option<&Decl> {
match stmt {
Stmt::Decl(d) => Some(d),
_ => None,
}
}
fn find_best_binding_impl<'a, F>(
stmts: &'a [Option<&'a Stmt>],
name: &str,
mut decl_at: F,
) -> Option<RefTarget>
where
F: FnMut(usize) -> Option<&'a Decl>,
{
let mut fn_decl_idx: Option<usize> = None;
let mut last_assign: Option<(usize, usize)> = None;
let mut var_decl: Option<(usize, usize)> = None;
for (idx, _) in stmts.iter().enumerate() {
if let Some(decl) = decl_at(idx) {
match decl {
Decl::Fn(fd) if fd.ident.sym.as_ref() == name && fn_decl_idx.is_none() => {
fn_decl_idx = Some(idx);
}
Decl::Var(v) => {
for (di, d) in v.decls.iter().enumerate() {
if let Pat::Ident(bi) = &d.name {
if bi.id.sym.as_ref() == name && d.init.is_some() && var_decl.is_none()
{
var_decl = Some((idx, di));
}
}
}
}
_ => {}
}
}
if let Some(stmt) = stmts.get(idx).and_then(|s| s.as_ref()) {
let mut assign_finder = AssignToNameFinder {
name,
count: 0,
last_seq_idx: None,
};
stmt.visit_with(&mut assign_finder);
if let Some(seq_idx) = assign_finder.last_seq_idx {
last_assign = Some((idx, seq_idx));
}
}
}
if let Some(idx) = fn_decl_idx {
return Some(RefTarget::FnDecl(idx));
}
if let Some((idx, seq)) = last_assign {
return Some(RefTarget::Assign {
stmt_idx: idx,
assign_idx_in_seq: seq,
});
}
if let Some((idx, di)) = var_decl {
return Some(RefTarget::VarDecl {
stmt_idx: idx,
declarator_idx: di,
});
}
None
}
struct AssignToNameFinder<'a> {
name: &'a str,
count: usize,
last_seq_idx: Option<usize>,
}
impl Visit for AssignToNameFinder<'_> {
fn visit_assign_expr(&mut self, node: &AssignExpr) {
if matches!(node.op, AssignOp::Assign) {
if let AssignTarget::Simple(SimpleAssignTarget::Ident(bi)) = &node.left {
if bi.id.sym.as_ref() == self.name {
self.last_seq_idx = Some(self.count);
self.count += 1;
}
}
}
node.visit_children_with(self);
}
}
fn tag_best_binding_in_items(items: &mut [ModuleItem], name: &str, accept_obj: bool) {
let Some(target) = find_best_binding_items(items, name) else {
return;
};
match target {
RefTarget::FnDecl(idx) => {
if let Some(Decl::Fn(fd)) = item_as_decl_mut(&mut items[idx]) {
if let Some(body) = fd.function.body.as_mut() {
push_dir(body);
}
}
}
RefTarget::VarDecl {
stmt_idx,
declarator_idx,
} => {
if let Some(Decl::Var(v)) = item_as_decl_mut(&mut items[stmt_idx]) {
if let Some(d) = v.decls.get_mut(declarator_idx) {
if let Some(init) = d.init.as_mut() {
tag_expr_for_hook_ref(init, accept_obj);
}
}
}
}
RefTarget::Assign {
stmt_idx,
assign_idx_in_seq,
} => {
if let Some(stmt) = item_as_stmt_mut(&mut items[stmt_idx]) {
tag_nth_assign_rhs(stmt, name, assign_idx_in_seq, accept_obj);
}
}
}
}
fn tag_best_binding_in_stmts(stmts: &mut [Stmt], name: &str, accept_obj: bool) {
let Some(target) = find_best_binding_stmts(stmts, name) else {
return;
};
match target {
RefTarget::FnDecl(idx) => {
if let Stmt::Decl(Decl::Fn(fd)) = &mut stmts[idx] {
if let Some(body) = fd.function.body.as_mut() {
push_dir(body);
}
}
}
RefTarget::VarDecl {
stmt_idx,
declarator_idx,
} => {
if let Stmt::Decl(Decl::Var(v)) = &mut stmts[stmt_idx] {
if let Some(d) = v.decls.get_mut(declarator_idx) {
if let Some(init) = d.init.as_mut() {
tag_expr_for_hook_ref(init, accept_obj);
}
}
}
}
RefTarget::Assign {
stmt_idx,
assign_idx_in_seq,
} => {
tag_nth_assign_rhs(&mut stmts[stmt_idx], name, assign_idx_in_seq, accept_obj);
}
}
}
fn item_as_decl_mut(item: &mut ModuleItem) -> Option<&mut Decl> {
match item {
ModuleItem::Stmt(Stmt::Decl(d)) => Some(d),
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) => Some(&mut e.decl),
_ => None,
}
}
fn item_as_stmt_mut(item: &mut ModuleItem) -> Option<&mut Stmt> {
match item {
ModuleItem::Stmt(s) => Some(s),
_ => None,
}
}
fn tag_expr_for_hook_ref(expr: &mut Expr, accept_obj: bool) {
match expr {
Expr::Fn(fn_expr) => {
if let Some(body) = fn_expr.function.body.as_mut() {
push_dir(body);
}
}
Expr::Arrow(arrow) => {
ensure_block_body(arrow);
if let BlockStmtOrExpr::BlockStmt(b) = arrow.body.as_mut() {
push_dir(b);
}
}
Expr::Object(obj) if accept_obj => {
for prop in &mut obj.props {
if let PropOrSpread::Prop(p) = prop {
match p.as_mut() {
Prop::Method(m) => {
if let Some(body) = m.function.body.as_mut() {
push_dir(body);
}
}
Prop::KeyValue(kv) => {
tag_expr_for_hook_ref(&mut kv.value, false);
}
_ => {}
}
}
}
}
_ => {}
}
}
fn tag_nth_assign_rhs(stmt: &mut Stmt, name: &str, nth: usize, accept_obj: bool) {
let mut tagger = AssignTagger {
name,
nth,
count: 0,
accept_obj,
done: false,
};
stmt.visit_mut_with(&mut tagger);
}
struct AssignTagger<'a> {
name: &'a str,
nth: usize,
count: usize,
accept_obj: bool,
done: bool,
}
impl VisitMut for AssignTagger<'_> {
fn visit_mut_assign_expr(&mut self, node: &mut AssignExpr) {
if self.done {
return;
}
if matches!(node.op, AssignOp::Assign) {
if let AssignTarget::Simple(SimpleAssignTarget::Ident(bi)) = &node.left {
if bi.id.sym.as_ref() == self.name {
if self.count == self.nth {
tag_expr_for_hook_ref(&mut node.right, self.accept_obj);
self.done = true;
return;
}
self.count += 1;
}
}
}
node.visit_mut_children_with(self);
}
}
fn add_worklet_dir_item(item: &mut ModuleItem) {
match item {
ModuleItem::Stmt(s) => add_worklet_dir_stmt(s),
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) => add_worklet_dir_decl(&mut e.decl),
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(e)) => {
add_worklet_dir_expr(&mut e.expr)
}
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(e)) => match &mut e.decl {
DefaultDecl::Fn(fn_expr) => {
if let Some(b) = fn_expr.function.body.as_mut() {
push_dir(b);
}
}
DefaultDecl::Class(class_expr) => {
add_worklet_dir_class(&mut class_expr.class);
}
DefaultDecl::TsInterfaceDecl(_) => {}
},
_ => {}
}
}
fn add_worklet_dir_stmt(stmt: &mut Stmt) {
match stmt {
Stmt::Decl(d) => add_worklet_dir_decl(d),
Stmt::Expr(ExprStmt { expr, .. }) => add_worklet_dir_expr(expr),
_ => {}
}
}
fn add_worklet_dir_decl(decl: &mut Decl) {
match decl {
Decl::Fn(fn_decl) => {
if let Some(b) = fn_decl.function.body.as_mut() {
push_dir(b);
}
}
Decl::Var(v) => {
for d in &mut v.decls {
if let Some(init) = &mut d.init {
add_worklet_dir_expr(init);
}
}
}
Decl::Class(class_decl) => {
add_worklet_dir_class(&mut class_decl.class);
}
_ => {}
}
}
fn add_worklet_dir_expr(expr: &mut Expr) {
match expr {
Expr::Fn(fn_expr) => {
if let Some(b) = fn_expr.function.body.as_mut() {
push_dir(b);
}
}
Expr::Arrow(arrow) => match arrow.body.as_mut() {
BlockStmtOrExpr::BlockStmt(b) => push_dir(b),
BlockStmtOrExpr::Expr(inner) => {
let i = inner.clone();
let mut block = BlockStmt {
span: DUMMY_SP,
stmts: vec![Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(i),
})],
ctxt: Default::default(),
};
push_dir(&mut block);
*arrow.body = BlockStmtOrExpr::BlockStmt(block);
}
},
Expr::Object(obj) => add_worklet_dir_object(obj),
Expr::Class(class_expr) => {
add_worklet_dir_class(&mut class_expr.class);
}
_ => {}
}
}
fn add_worklet_dir_class(class: &mut Class) {
for member in &mut class.body {
match member {
ClassMember::Method(m) => {
if let Some(body) = m.function.body.as_mut() {
push_dir(body);
}
}
ClassMember::Constructor(c) => {
if let Some(body) = c.body.as_mut() {
push_dir(body);
}
}
ClassMember::ClassProp(p) => {
if let Some(value) = p.value.as_mut() {
add_worklet_dir_expr(value);
}
}
_ => {}
}
}
}
fn add_worklet_dir_object(obj: &mut ObjectLit) {
if object_lit_has_this_method(obj) {
if !obj.props.iter().any(is_context_object_marker) {
obj.props
.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(IdentName::new(CONTEXT_OBJECT_MARKER.into(), DUMMY_SP)),
value: Box::new(Expr::Lit(Lit::Bool(Bool {
span: DUMMY_SP,
value: true,
}))),
}))));
}
return;
}
for prop in &mut obj.props {
if let PropOrSpread::Prop(p) = prop {
match p.as_mut() {
Prop::Method(m) => {
if let Some(body) = m.function.body.as_mut() {
push_dir(body);
}
}
Prop::KeyValue(kv) => {
add_worklet_dir_expr(&mut kv.value);
}
_ => {}
}
}
}
}
fn object_lit_has_this_method(obj: &ObjectLit) -> bool {
obj.props.iter().any(|prop| {
let PropOrSpread::Prop(p) = prop else {
return false;
};
let Prop::Method(m) = p.as_ref() else {
return false;
};
let Some(body) = m.function.body.as_ref() else {
return false;
};
let mut seeker = ThisFinder { found: false };
body.visit_with(&mut seeker);
seeker.found
})
}
struct ThisFinder {
found: bool,
}
impl Visit for ThisFinder {
fn visit_this_expr(&mut self, _: &ThisExpr) {
self.found = true;
}
fn visit_function(&mut self, _: &Function) {}
fn visit_class(&mut self, _: &Class) {}
}
fn push_dir(block: &mut BlockStmt) {
if !block
.stmts
.iter()
.any(|s| str_stmt_value(s) == Some(WORKLET_DIRECTIVE))
{
block.stmts.insert(
0,
Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: WORKLET_DIRECTIVE.into(),
raw: None,
}))),
}),
);
}
}
fn is_generated_worklet_file(filename: &str) -> bool {
to_posix_path(filename).contains(&format!("react-native-worklets/{GENERATED_WORKLETS_DIR}"))
}
fn to_posix_path(path: &str) -> String {
path.replace('\\', "/")
}
fn rename_free_refs(body: &mut BlockStmt, from: &str, to: &str) -> bool {
let mut declared: IndexSet<Atom> = IndexSet::new();
let mut decl_collector = DeclCollector {
declared: &mut declared,
depth: 0,
};
body.visit_with(&mut decl_collector);
let from_atom = Atom::from(from);
if declared.contains(&from_atom) {
return false;
}
let mut renamer = FreeRefRenamer {
from: from_atom,
to: Atom::from(to),
declared,
renamed: false,
};
body.visit_mut_with(&mut renamer);
renamer.renamed
}
struct FreeRefRenamer {
from: Atom,
to: Atom,
declared: IndexSet<Atom>,
renamed: bool,
}
impl FreeRefRenamer {
fn with_inner<F: FnOnce(&mut Self)>(&mut self, extra: &[Atom], f: F) {
let mut added: Vec<Atom> = vec![];
for name in extra {
if !self.declared.contains(name) {
self.declared.insert(name.clone());
added.push(name.clone());
}
}
f(self);
for name in &added {
self.declared.shift_remove(name);
}
}
}
impl VisitMut for FreeRefRenamer {
fn visit_mut_ident(&mut self, ident: &mut Ident) {
if ident.sym == self.from && !self.declared.contains(&self.from) {
ident.sym = self.to.clone();
ident.ctxt = SyntaxContext::empty();
self.renamed = true;
}
}
fn visit_mut_member_expr(&mut self, node: &mut MemberExpr) {
node.obj.visit_mut_with(self);
if let MemberProp::Computed(c) = &mut node.prop {
c.expr.visit_mut_with(self);
}
}
fn visit_mut_prop(&mut self, prop: &mut Prop) {
match prop {
Prop::Shorthand(id) => {
self.visit_mut_ident(id);
}
Prop::KeyValue(kv) => {
if let PropName::Computed(c) = &mut kv.key {
c.expr.visit_mut_with(self);
}
kv.value.visit_mut_with(self);
}
Prop::Assign(a) => {
a.value.visit_mut_with(self);
}
Prop::Getter(g) => {
if let PropName::Computed(c) = &mut g.key {
c.expr.visit_mut_with(self);
}
if let Some(body) = g.body.as_mut() {
body.visit_mut_with(self);
}
}
Prop::Setter(s) => {
if let PropName::Computed(c) = &mut s.key {
c.expr.visit_mut_with(self);
}
let mut extras: IndexSet<Atom> = IndexSet::new();
collect_pat_bindings(&s.param, &mut extras);
let extras_vec: Vec<Atom> = extras.into_iter().collect();
self.with_inner(&extras_vec, |v| {
if let Some(body) = s.body.as_mut() {
body.visit_mut_with(v);
}
});
}
Prop::Method(m) => {
if let PropName::Computed(c) = &mut m.key {
c.expr.visit_mut_with(self);
}
let mut extras: IndexSet<Atom> = IndexSet::new();
for p in &m.function.params {
collect_pat_bindings(&p.pat, &mut extras);
}
if let Some(body) = &m.function.body {
let mut dc = DeclCollector {
declared: &mut extras,
depth: 0,
};
body.visit_with(&mut dc);
}
let extras_vec: Vec<Atom> = extras.into_iter().collect();
self.with_inner(&extras_vec, |v| {
if let Some(body) = m.function.body.as_mut() {
body.visit_mut_with(v);
}
});
}
}
}
fn visit_mut_function(&mut self, node: &mut Function) {
let mut extras: IndexSet<Atom> = IndexSet::new();
for p in &node.params {
collect_pat_bindings(&p.pat, &mut extras);
}
if let Some(body) = &node.body {
let mut dc = DeclCollector {
declared: &mut extras,
depth: 0,
};
body.visit_with(&mut dc);
}
let extras_vec: Vec<Atom> = extras.into_iter().collect();
self.with_inner(&extras_vec, |v| {
if let Some(body) = node.body.as_mut() {
body.visit_mut_with(v);
}
});
}
fn visit_mut_arrow_expr(&mut self, node: &mut ArrowExpr) {
let mut extras: IndexSet<Atom> = IndexSet::new();
for p in &node.params {
collect_pat_bindings(p, &mut extras);
}
if let BlockStmtOrExpr::BlockStmt(block) = &*node.body {
let mut dc = DeclCollector {
declared: &mut extras,
depth: 0,
};
block.visit_with(&mut dc);
}
let extras_vec: Vec<Atom> = extras.into_iter().collect();
self.with_inner(&extras_vec, |v| {
node.body.visit_mut_with(v);
});
}
fn visit_mut_ts_type(&mut self, _: &mut TsType) {}
fn visit_mut_ts_type_ann(&mut self, _: &mut TsTypeAnn) {}
fn visit_mut_ts_type_param_decl(&mut self, _: &mut TsTypeParamDecl) {}
fn visit_mut_ts_type_param_instantiation(&mut self, _: &mut TsTypeParamInstantiation) {}
}
fn make_stack_details_decl() -> Stmt {
let new_error = ident_expr("global")
.make_member(crate::factory::ident_name("Error"))
.into_new_expr(DUMMY_SP, Some(vec![]));
let neg27 = Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: UnaryOp::Minus,
arg: Box::new(num_lit(27.0)),
});
let arr = Expr::Array(ArrayLit {
span: DUMMY_SP,
elems: vec![
Some(Expr::New(new_error).as_arg()),
Some(num_lit(1.0).as_arg()),
Some(neg27.as_arg()),
],
});
Stmt::Decl(Decl::Var(Box::new(
arr.into_var_decl(VarDeclKind::Const, crate::factory::binding("_e")),
)))
}
fn make_init_data_decl(name: &str, code: &str, location: &str, source_map: Option<&str>) -> Stmt {
let kv = |key: &str, value: &str| -> PropOrSpread {
PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(crate::factory::ident_name(key)),
value: Box::new(str_lit(value)),
})))
};
let mut props = vec![kv("code", code)];
if !location.is_empty() {
props.push(kv("location", location));
}
if let Some(sm) = source_map {
props.push(kv("sourceMap", sm));
}
let init = Expr::Object(ObjectLit {
span: DUMMY_SP,
props,
});
Stmt::Decl(Decl::Var(Box::new(
init.into_var_decl(VarDeclKind::Const, crate::factory::binding(name)),
)))
}
fn factory_param(cv: &[Ident], init_id: Option<&str>) -> Param {
let assign_prop = |sym: &str| {
ObjectPatProp::Assign(AssignPatProp {
span: DUMMY_SP,
key: BindingIdent {
id: id(sym),
type_ann: None,
},
value: None,
})
};
let mut props = Vec::with_capacity(cv.len() + if init_id.is_some() { 1 } else { 0 });
if let Some(init_id) = init_id {
props.push(assign_prop(init_id));
}
props.extend(cv.iter().map(|v| assign_prop(v.sym.as_ref())));
Param {
span: DUMMY_SP,
decorators: vec![],
pat: Pat::Object(ObjectPat {
span: DUMMY_SP,
props,
optional: false,
type_ann: None,
}),
}
}
fn factory_call_obj(cv: &[Ident], init_id: Option<&str>) -> Expr {
let mut props = Vec::with_capacity(cv.len() + if init_id.is_some() { 1 } else { 0 });
if let Some(init_id) = init_id {
props.push(shorthand_prop(init_id));
}
for v in cv {
let value: Expr = if let Some(class_name) = strip_class_factory_suffix(v.sym.as_ref()) {
let class_ident = Ident::new(class_name.into(), v.span, v.ctxt);
Expr::Member(MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::Ident(class_ident)),
prop: MemberProp::Ident(IdentName::new(v.sym.clone(), DUMMY_SP)),
})
} else {
Expr::Ident(v.clone())
};
props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(IdentName::new(v.sym.clone(), DUMMY_SP)),
value: Box::new(value),
}))));
}
Expr::Object(ObjectLit {
span: DUMMY_SP,
props,
})
}
fn strip_class_factory_suffix(name: &str) -> Option<&str> {
name.strip_suffix(WORKLET_CLASS_FACTORY_SUFFIX)
.filter(|stripped| !stripped.is_empty())
}
fn substitute_worklet_class_news(body: &mut BlockStmt, closure_vars: &mut Vec<Ident>) {
if closure_vars.is_empty() {
return;
}
let captured: FxHashSet<Atom> = closure_vars.iter().map(|i| i.sym.clone()).collect();
let mut visitor = NewClassRefCollector {
captured: &captured,
found: IndexSet::new(),
};
body.visit_with(&mut visitor);
if visitor.found.is_empty() {
return;
}
for class_name in visitor.found.iter() {
let Some(pos) = closure_vars.iter().position(|v| &v.sym == class_name) else {
continue;
};
let original = closure_vars.remove(pos);
let factory_atom: Atom =
format!("{}{}", class_name.as_ref(), WORKLET_CLASS_FACTORY_SUFFIX).into();
let factory_ident = Ident::new(factory_atom.clone(), original.span, original.ctxt);
closure_vars.push(factory_ident);
let class_ident = Ident::new(class_name.clone(), original.span, original.ctxt);
let factory_invocation = Expr::Call(CallExpr {
span: DUMMY_SP,
ctxt: Default::default(),
callee: Callee::Expr(Box::new(Expr::Ident(Ident::new(
factory_atom,
original.span,
original.ctxt,
)))),
args: vec![],
type_args: None,
});
let const_decl = Stmt::Decl(Decl::Var(Box::new(VarDecl {
span: DUMMY_SP,
ctxt: Default::default(),
kind: VarDeclKind::Const,
declare: false,
decls: vec![VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(BindingIdent {
id: class_ident,
type_ann: None,
}),
init: Some(Box::new(factory_invocation)),
definite: false,
}],
})));
body.stmts.insert(0, const_decl);
}
}
struct NewClassRefCollector<'a> {
captured: &'a FxHashSet<Atom>,
found: IndexSet<Atom>,
}
impl Visit for NewClassRefCollector<'_> {
fn visit_new_expr(&mut self, node: &NewExpr) {
if let Expr::Ident(ident) = node.callee.as_ref() {
if self.captured.contains(&ident.sym) {
self.found.insert(ident.sym.clone());
}
}
node.callee.visit_with(self);
if let Some(args) = &node.args {
for arg in args {
arg.visit_with(self);
}
}
}
}
pub(crate) fn prop_name_str(name: &PropName) -> Option<&str> {
match name {
PropName::Ident(id) => Some(id.sym.as_ref()),
PropName::Str(s) => s.value.as_str(),
_ => None,
}
}
fn pat_ident(pat: &Pat) -> Option<&str> {
if let Pat::Ident(id) = pat {
Some(id.id.sym.as_ref())
} else {
None
}
}
fn callee_ident(callee: &Callee) -> Option<&str> {
match callee {
Callee::Expr(e) => match e.as_ref() {
Expr::Ident(id) => Some(id.sym.as_ref()),
Expr::Member(me) => {
if let MemberProp::Ident(p) = &me.prop {
Some(p.sym.as_ref())
} else {
None
}
}
_ => None,
},
_ => None,
}
}
fn is_worklet_class_marker(member: &ClassMember) -> bool {
let ClassMember::ClassProp(p) = member else {
return false;
};
prop_name_str(&p.key) == Some(WORKLET_CLASS_MARKER)
}
fn class_has_worklet_marker(class: &Class) -> bool {
class.body.iter().any(is_worklet_class_marker)
}
fn detect_marked_class_decl_ident(item: &ModuleItem, options: &WorkletsOptions) -> Option<Ident> {
if options.disable_worklet_classes || options.bundle_mode {
return None;
}
match item {
ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl))) => {
class_has_worklet_marker(&class_decl.class).then(|| class_decl.ident.clone())
}
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Class(class_decl),
..
})) => class_has_worklet_marker(&class_decl.class).then(|| class_decl.ident.clone()),
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
decl: DefaultDecl::Class(class_expr),
..
})) => {
let ident = class_expr.ident.as_ref()?;
class_has_worklet_marker(&class_expr.class).then(|| ident.clone())
}
_ => None,
}
}
fn detect_marked_class_decl_ident_stmt(stmt: &Stmt, options: &WorkletsOptions) -> Option<Ident> {
if options.disable_worklet_classes || options.bundle_mode {
return None;
}
if let Stmt::Decl(Decl::Class(class_decl)) = stmt {
if class_has_worklet_marker(&class_decl.class) {
return Some(class_decl.ident.clone());
}
}
None
}
enum ClassExportKind {
None,
Named(swc_common::Span),
Default(swc_common::Span),
}
fn take_class_expr_from_item(item: ModuleItem) -> Option<(Box<Class>, ClassExportKind)> {
match item {
ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl))) => {
Some((class_decl.class, ClassExportKind::None))
}
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Class(class_decl),
span,
})) => Some((class_decl.class, ClassExportKind::Named(span))),
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
decl: DefaultDecl::Class(class_expr),
span,
})) => Some((class_expr.class, ClassExportKind::Default(span))),
_ => None,
}
}
fn take_class_expr_from_stmt(stmt: Stmt) -> Option<Box<Class>> {
if let Stmt::Decl(Decl::Class(class_decl)) = stmt {
Some(class_decl.class)
} else {
None
}
}
fn build_class_factory_body(
class_ident: &Ident,
factory_ident: &Ident,
class_node: Box<Class>,
) -> BlockStmt {
let (ctor_decl_stmt, prototype_assigns) =
lower_class_to_constructor_fn(class_ident, class_node);
let assign_back_stmt = Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP,
op: AssignOp::Assign,
left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::Ident(class_ident.clone())),
prop: MemberProp::Ident(IdentName {
span: DUMMY_SP,
sym: factory_ident.sym.clone(),
}),
})),
right: Box::new(Expr::Ident(factory_ident.clone())),
})),
});
let return_stmt = Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(Box::new(Expr::Ident(class_ident.clone()))),
});
let mut stmts: Vec<Stmt> = Vec::with_capacity(4 + prototype_assigns.len());
stmts.push(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: WORKLET_DIRECTIVE.into(),
raw: None,
}))),
}));
stmts.push(ctor_decl_stmt);
stmts.extend(prototype_assigns);
stmts.push(assign_back_stmt);
stmts.push(return_stmt);
BlockStmt {
span: DUMMY_SP,
ctxt: Default::default(),
stmts,
}
}
#[allow(clippy::boxed_local)] fn lower_class_to_constructor_fn(class_ident: &Ident, class_node: Box<Class>) -> (Stmt, Vec<Stmt>) {
let mut ctor_params: Vec<Param> = vec![];
let mut field_assigns: Vec<Stmt> = vec![];
let mut prototype_assigns: Vec<Stmt> = vec![];
let mut ctor_body_stmts: Vec<Stmt> = vec![];
for member in class_node.body {
match member {
ClassMember::ClassProp(prop) => {
let Some(value) = prop.value else {
continue;
};
let key = match prop.key {
PropName::Ident(i) => MemberProp::Ident(i),
PropName::Str(s) => MemberProp::Computed(ComputedPropName {
span: DUMMY_SP,
expr: Box::new(Expr::Lit(Lit::Str(s))),
}),
PropName::Num(n) => MemberProp::Computed(ComputedPropName {
span: DUMMY_SP,
expr: Box::new(Expr::Lit(Lit::Num(n))),
}),
PropName::Computed(c) => MemberProp::Computed(c),
PropName::BigInt(_) => continue,
#[cfg(swc_ast_unknown)]
_ => continue,
};
if is_method_factory_call(&value) {
prototype_assigns.push(make_prototype_assign(class_ident, key, *value));
} else {
field_assigns.push(make_this_assign(key, *value));
}
}
ClassMember::Constructor(ctor) => {
for p in ctor.params {
match p {
ParamOrTsParamProp::Param(param) => ctor_params.push(param),
ParamOrTsParamProp::TsParamProp(_) => {}
}
}
if let Some(body) = ctor.body {
ctor_body_stmts.extend(body.stmts);
}
}
_ => {}
}
}
let mut body_stmts = field_assigns;
body_stmts.extend(ctor_body_stmts);
let func = Function {
params: ctor_params,
decorators: vec![],
span: DUMMY_SP,
ctxt: Default::default(),
body: Some(BlockStmt {
span: DUMMY_SP,
ctxt: Default::default(),
stmts: body_stmts,
}),
is_generator: false,
is_async: false,
type_params: None,
return_type: None,
};
let ctor_decl = Stmt::Decl(Decl::Var(Box::new(VarDecl {
span: DUMMY_SP,
ctxt: Default::default(),
kind: VarDeclKind::Var,
declare: false,
decls: vec![VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(BindingIdent {
id: class_ident.clone(),
type_ann: None,
}),
init: Some(Box::new(Expr::Fn(FnExpr {
ident: Some(class_ident.clone()),
function: Box::new(func),
}))),
definite: false,
}],
})));
(ctor_decl, prototype_assigns)
}
fn is_method_factory_call(value: &Expr) -> bool {
let Expr::Call(call) = value else {
return false;
};
let Callee::Expr(callee) = &call.callee else {
return false;
};
let mut inner = callee.as_ref();
while let Expr::Paren(p) = inner {
inner = p.expr.as_ref();
}
matches!(inner, Expr::Fn(_))
}
fn make_prototype_assign(class_ident: &Ident, prop: MemberProp, value: Expr) -> Stmt {
let prototype_member = Expr::Member(MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::Ident(class_ident.clone())),
prop: MemberProp::Ident(IdentName::new("prototype".into(), DUMMY_SP)),
});
Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP,
op: AssignOp::Assign,
left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr {
span: DUMMY_SP,
obj: Box::new(prototype_member),
prop,
})),
right: Box::new(value),
})),
})
}
fn make_this_assign(prop: MemberProp, value: Expr) -> Stmt {
Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP,
op: AssignOp::Assign,
left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::This(ThisExpr { span: DUMMY_SP })),
prop,
})),
right: Box::new(value),
})),
})
}
fn build_factory_invocation_var_decl(class_ident: &Ident) -> VarDecl {
let factory_name = format!(
"{}{}",
class_ident.sym.as_ref(),
WORKLET_CLASS_FACTORY_SUFFIX
);
let factory_ident = Ident::from(Atom::from(factory_name.as_str()));
let call = Expr::Call(CallExpr {
span: DUMMY_SP,
ctxt: Default::default(),
callee: Callee::Expr(Box::new(Expr::Ident(factory_ident))),
args: vec![],
type_args: None,
});
VarDecl {
span: DUMMY_SP,
ctxt: Default::default(),
kind: VarDeclKind::Const,
declare: false,
decls: vec![VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(BindingIdent {
id: class_ident.clone(),
type_ann: None,
}),
init: Some(Box::new(call)),
definite: false,
}],
}
}
fn is_context_object_marker(prop: &PropOrSpread) -> bool {
let PropOrSpread::Prop(prop_box) = prop else {
return false;
};
let name = match prop_box.as_ref() {
Prop::KeyValue(kv) => prop_name_str(&kv.key),
Prop::Shorthand(id) => Some(id.sym.as_ref()),
Prop::Method(m) => prop_name_str(&m.key),
_ => None,
};
name == Some(CONTEXT_OBJECT_MARKER)
}
fn str_stmt_value(s: &Stmt) -> Option<&str> {
if let Stmt::Expr(ExprStmt { expr, .. }) = s {
if let Expr::Lit(Lit::Str(sv)) = expr.as_ref() {
return sv.value.as_str();
}
}
None
}
fn sanitize_ident(s: &str) -> String {
fn is_ident_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '$'
}
let mapped: String = s
.chars()
.map(|c| if is_ident_char(c) { c } else { '-' })
.collect();
let trimmed: String = {
let mut it = mapped.chars().peekable();
while let Some(&c) = it.peek() {
if c == '-' || c.is_ascii_digit() {
it.next();
} else {
break;
}
}
it.collect()
};
let mut out = String::with_capacity(trimmed.len());
let mut chars = trimmed.chars().peekable();
while let Some(c) = chars.next() {
if c == '-' || c.is_whitespace() {
while let Some(&nc) = chars.peek() {
if nc == '-' || nc.is_whitespace() {
chars.next();
} else {
break;
}
}
if let Some(nc) = chars.next() {
for up in nc.to_uppercase() {
out.push(up);
}
}
} else {
out.push(c);
}
}
if out.is_empty() {
return "_".to_string();
}
if out
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
return format!("_{out}");
}
if !is_valid_identifier(&out) {
return format!("_{out}");
}
out
}
fn is_valid_identifier(name: &str) -> bool {
!is_reserved_word(name)
}
fn is_reserved_word(name: &str) -> bool {
matches!(
name,
"break" | "case" | "catch" | "continue" | "debugger" | "default"
| "do" | "else" | "finally" | "for" | "function" | "if"
| "return" | "switch" | "throw" | "try" | "var" | "const"
| "while" | "with" | "new" | "this" | "super" | "class"
| "extends" | "export" | "import" | "null" | "true" | "false"
| "in" | "instanceof" | "typeof" | "void" | "delete"
| "implements" | "interface" | "let" | "package" | "private"
| "protected" | "public" | "static" | "yield"
| "await" | "enum"
)
}
fn ensure_block_body(arrow: &mut ArrowExpr) {
if let BlockStmtOrExpr::Expr(e) = arrow.body.as_mut() {
let inner = e.clone();
*arrow.body = BlockStmtOrExpr::BlockStmt(BlockStmt {
span: DUMMY_SP,
stmts: vec![Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(inner),
})],
ctxt: Default::default(),
});
}
}
#[allow(clippy::too_many_arguments)]
fn build_worklet_code_and_map(
worklet_name: &str,
params: &[Param],
body: &BlockStmt,
cv: &[Ident],
is_generator: bool,
is_async: bool,
cm: Option<&Lrc<SourceMap>>,
location: &str,
) -> (String, Option<String>) {
let mut stmts = body.stmts.clone();
if !cv.is_empty() {
stmts.insert(0, make_closure_destruct_stmt(cv));
}
let fn_expr = Expr::Fn(FnExpr {
ident: Some(id(worklet_name)),
function: Box::new(Function {
params: params.to_vec(),
decorators: vec![],
span: DUMMY_SP,
ctxt: Default::default(),
body: Some(BlockStmt {
span: DUMMY_SP,
stmts,
ctxt: Default::default(),
}),
is_generator,
is_async,
type_params: None,
return_type: None,
}),
});
match cm {
Some(cm) => emit_expr_with_source_map(&fn_expr, cm, location),
None => (
emit_expr_str(&fn_expr, &Lrc::new(SourceMap::default())),
None,
),
}
}
fn make_closure_destruct_stmt(cv: &[Ident]) -> Stmt {
Stmt::Decl(Decl::Var(Box::new(VarDecl {
span: DUMMY_SP,
ctxt: Default::default(),
kind: VarDeclKind::Const,
declare: false,
decls: vec![VarDeclarator {
span: DUMMY_SP,
name: Pat::Object(ObjectPat {
span: DUMMY_SP,
props: cv
.iter()
.map(|v| {
ObjectPatProp::Assign(AssignPatProp {
span: DUMMY_SP,
key: BindingIdent {
id: Ident::new(v.sym.clone(), DUMMY_SP, v.ctxt),
type_ann: None,
},
value: None,
})
})
.collect(),
optional: false,
type_ann: None,
}),
init: Some(Box::new(Expr::Member(MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::This(ThisExpr { span: DUMMY_SP })),
prop: MemberProp::Ident(IdentName::new("__closure".into(), DUMMY_SP)),
}))),
definite: false,
}],
})))
}
fn worklet_module_for(expr: &Expr) -> Module {
Module {
span: DUMMY_SP,
body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(expr.clone()),
}))],
shebang: None,
}
}
pub fn emit_expr_str(expr: &Expr, cm: &Lrc<SourceMap>) -> String {
let module = crate::transform::transform_worklet(worklet_module_for(expr));
let mut buf: Vec<u8> = vec![];
{
let wr = JsWriter::new(cm.clone(), "\n", &mut buf, None);
let mut emitter = Emitter {
cfg: EmitConfig::default(),
cm: cm.clone(),
comments: None,
wr,
};
emitter.emit_module(&module).ok();
}
let s = unsafe { String::from_utf8_unchecked(buf) };
s.trim_end_matches([';', '\n', '\r', ' ']).to_string()
}
fn emit_expr_with_source_map(
expr: &Expr,
cm: &Lrc<SourceMap>,
location: &str,
) -> (String, Option<String>) {
let module = crate::transform::transform_worklet(worklet_module_for(expr));
let mut buf: Vec<u8> = vec![];
let mut mappings: Vec<(BytePos, LineCol)> = vec![];
{
let wr = JsWriter::new(cm.clone(), "\n", &mut buf, Some(&mut mappings));
let mut emitter = Emitter {
cfg: EmitConfig::default(),
cm: cm.clone(),
comments: None,
wr,
};
emitter.emit_module(&module).ok();
}
let code = unsafe { String::from_utf8_unchecked(buf) };
let code = code.trim_end_matches([';', '\n', '\r', ' ']).to_string();
let sm_json = if mappings.is_empty() {
None
} else {
let config = WorkletSourceMapConfig {
override_source: if location.is_empty() {
None
} else {
Some(location.to_string())
},
};
let sm = cm.build_source_map(&mappings, None, config);
let mut sm_buf: Vec<u8> = vec![];
sm.to_writer(&mut sm_buf)
.ok()
.and_then(|()| String::from_utf8(sm_buf).ok())
};
(code, sm_json)
}
struct WorkletSourceMapConfig {
override_source: Option<String>,
}
impl swc_common::source_map::SourceMapGenConfig for WorkletSourceMapConfig {
fn file_name_to_source(&self, f: &swc_common::FileName) -> String {
if let Some(ref s) = self.override_source {
return s.clone();
}
DefaultSourceMapGenConfig.file_name_to_source(f)
}
fn inline_sources_content(&self, _f: &swc_common::FileName) -> bool {
false
}
}