Support default arrow function export in server boundary (#46977

This PR adds basic support for default export of an async arrow
function:

```js
export default async (a, b) => { console.log(a, b) }
```

In upcoming PRs I'll refactor named export handling and inlined
declarations.
This commit is contained in:
Shu Ding 2023-03-10 14:38:51 +01:00 committed by GitHub
parent 10f2268f4e
commit d200e5fa1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 229 additions and 97 deletions

View file

@ -6,7 +6,7 @@ use next_binding::swc::core::{
comments::{Comment, CommentKind, Comments},
errors::HANDLER,
util::take::Take,
BytePos, FileName, DUMMY_SP,
BytePos, FileName, Span, DUMMY_SP,
},
ecma::{
ast::*,
@ -49,6 +49,7 @@ pub fn server_actions<C: Comments>(
action_idents: Default::default(),
async_fn_idents: Default::default(),
exported_idents: Default::default(),
action_arrow_span: Default::default(),
annotations: Default::default(),
extra_items: Default::default(),
@ -78,6 +79,10 @@ struct ServerActions<C: Comments> {
action_idents: Vec<Name>,
async_fn_idents: Vec<Id>,
// Since arrow functions don't have identifiers, we need to store the span
// to find the arrow function later.
action_arrow_span: Vec<Span>,
// (ident, is default export)
exported_idents: Vec<(Id, bool)>,
@ -87,10 +92,11 @@ struct ServerActions<C: Comments> {
}
impl<C: Comments> ServerActions<C> {
// Check if the function or arrow function is an action function
fn get_action_info(
&mut self,
ident: &mut Ident,
function: &mut Box<Function>,
maybe_ident: Option<&mut Ident>,
maybe_body: Option<&mut BlockStmt>,
) -> (bool, bool, bool) {
let mut is_action_fn = false;
let mut is_exported = self.in_export_decl;
@ -101,7 +107,7 @@ impl<C: Comments> ServerActions<C> {
is_action_fn = true;
} else {
// Check if the function has `"use server"`
if let Some(body) = &mut function.body {
if let Some(body) = maybe_body {
let directive_index = get_server_directive_index_in_fn(&body.stmts);
if directive_index >= 0 {
is_action_fn = true;
@ -109,15 +115,17 @@ impl<C: Comments> ServerActions<C> {
}
}
// If it's exported via named export, it's a valid action.
let exported_ident = self
.exported_idents
.iter()
.find(|(id, _)| id == &ident.to_id());
if let Some((_, is_default)) = exported_ident {
is_action_fn = true;
is_exported = true;
is_default_export = *is_default;
if let Some(ident) = maybe_ident {
// If it's exported via named export, it's a valid action.
let exported_ident = self
.exported_idents
.iter()
.find(|(id, _)| id == &ident.to_id());
if let Some((_, is_default)) = exported_ident {
is_action_fn = true;
is_exported = true;
is_default_export = *is_default;
}
}
}
@ -127,10 +135,11 @@ impl<C: Comments> ServerActions<C> {
fn add_action_annotations(
&mut self,
ident: &Ident,
function: &mut Box<Function>,
function: Option<&mut Box<Function>>,
arrow: Option<&mut ArrowExpr>,
is_exported: bool,
is_default_export: bool,
) -> Option<Box<Function>> {
) -> (Option<Box<Function>>, Option<Box<ArrowExpr>>) {
let need_rename_export = self.in_action_file && (self.in_export_decl || is_exported);
let action_name: JsWord = if need_rename_export {
ident.sym.clone()
@ -175,7 +184,7 @@ impl<C: Comments> ServerActions<C> {
self.annotations
.push(annotate(ident, "$$id", hex_encode(result).into()));
if self.top_level {
if self.top_level && arrow.is_none() {
// myAction.$$bound = [];
self.annotations.push(annotate(
ident,
@ -213,68 +222,122 @@ impl<C: Comments> ServerActions<C> {
let closure_arg = private_ident!("closure");
function.body.visit_mut_with(&mut ClosureReplacer {
closure_arg: &closure_arg,
used_ids: &ids_from_closure,
});
if let Some(a) = arrow {
a.visit_mut_with(&mut ClosureReplacer {
closure_arg: &closure_arg,
used_ids: &ids_from_closure,
});
// myAction.$$bound = [id1, id2]
self.annotations.push(annotate(
ident,
"$$bound",
ArrayLit {
span: DUMMY_SP,
elems: ids_from_closure
.iter()
.cloned()
.map(|id| Some(id.as_arg()))
.collect(),
}
.into(),
));
let call = CallExpr {
span: DUMMY_SP,
callee: action_ident.clone().as_callee(),
args: vec![ident.clone().make_member(quote_ident!("$$bound")).as_arg()],
type_args: Default::default(),
};
let new_fn = Box::new(Function {
params: function.params.clone(),
decorators: function.decorators.take(),
span: function.span,
body: Some(BlockStmt {
span: DUMMY_SP,
stmts: vec![Stmt::Return(ReturnStmt {
// myAction.$$bound = [id1, id2]
self.annotations.push(annotate(
ident,
"$$bound",
ArrayLit {
span: DUMMY_SP,
arg: Some(call.into()),
})],
}),
is_generator: function.is_generator,
is_async: function.is_async,
type_params: Default::default(),
return_type: Default::default(),
});
self.extra_items
.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
span: DUMMY_SP,
decl: FnDecl {
ident: action_ident,
function: Box::new(Function {
params: vec![closure_arg.into()],
..*function.take()
}),
declare: Default::default(),
elems: ids_from_closure
.iter()
.cloned()
.map(|id| Some(id.as_arg()))
.collect(),
}
.into(),
})));
));
return Some(new_fn);
let call = CallExpr {
span: DUMMY_SP,
callee: action_ident.clone().as_callee(),
args: vec![ident.clone().make_member(quote_ident!("$$bound")).as_arg()],
type_args: Default::default(),
};
let new_arrow = ArrowExpr {
span: DUMMY_SP,
params: a.params.clone(),
body: BlockStmtOrExpr::Expr(Box::new(Expr::Call(call))),
is_async: a.is_async,
is_generator: a.is_generator,
type_params: Default::default(),
return_type: Default::default(),
};
self.extra_items
.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: Default::default(),
decls: vec![VarDeclarator {
span: DUMMY_SP,
name: action_ident.into(),
init: None,
definite: Default::default(),
}],
})))));
return (None, Some(Box::new(new_arrow)));
} else if let Some(f) = function {
f.body.visit_mut_with(&mut ClosureReplacer {
closure_arg: &closure_arg,
used_ids: &ids_from_closure,
});
// myAction.$$bound = [id1, id2]
self.annotations.push(annotate(
ident,
"$$bound",
ArrayLit {
span: DUMMY_SP,
elems: ids_from_closure
.iter()
.cloned()
.map(|id| Some(id.as_arg()))
.collect(),
}
.into(),
));
let call = CallExpr {
span: DUMMY_SP,
callee: action_ident.clone().as_callee(),
args: vec![ident.clone().make_member(quote_ident!("$$bound")).as_arg()],
type_args: Default::default(),
};
let new_fn = Function {
params: f.params.clone(),
decorators: f.decorators.take(),
span: f.span,
body: Some(BlockStmt {
span: DUMMY_SP,
stmts: vec![Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(call.into()),
})],
}),
is_generator: f.is_generator,
is_async: f.is_async,
type_params: Default::default(),
return_type: Default::default(),
};
self.extra_items
.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
span: DUMMY_SP,
decl: FnDecl {
ident: action_ident,
function: Box::new(Function {
params: vec![closure_arg.into()],
..*f.take()
}),
declare: Default::default(),
}
.into(),
})));
return (Some(Box::new(new_fn)), None);
}
}
None
(None, None)
}
}
@ -296,6 +359,16 @@ impl<C: Comments> VisitMut for ServerActions<C> {
self.in_default_export_decl = old_default;
}
fn visit_mut_export_default_expr(&mut self, expr: &mut ExportDefaultExpr) {
let old = self.in_export_decl;
let old_default = self.in_default_export_decl;
self.in_export_decl = true;
self.in_default_export_decl = true;
expr.expr.visit_mut_with(self);
self.in_export_decl = old;
self.in_default_export_decl = old_default;
}
fn visit_mut_fn_expr(&mut self, f: &mut FnExpr) {
// Need to collect all async function identifiers if we are in a server
// file, because it can be exported later.
@ -321,7 +394,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
}
let (is_action_fn, is_exported, is_default_export) =
self.get_action_info(f.ident.as_mut().unwrap(), &mut f.function);
self.get_action_info(f.ident.as_mut(), f.function.body.as_mut());
{
// Visit children
@ -357,9 +430,10 @@ impl<C: Comments> VisitMut for ServerActions<C> {
.emit();
});
} else {
let maybe_new_fn = self.add_action_annotations(
let (maybe_new_fn, _) = self.add_action_annotations(
f.ident.as_mut().unwrap(),
&mut f.function,
Some(&mut f.function),
None,
is_exported,
is_default_export,
);
@ -381,7 +455,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
}
let (is_action_fn, is_exported, is_default_export) =
self.get_action_info(&mut f.ident, &mut f.function);
self.get_action_info(Some(&mut f.ident), f.function.body.as_mut());
{
// Visit children
@ -414,9 +488,10 @@ impl<C: Comments> VisitMut for ServerActions<C> {
.emit();
});
} else {
let maybe_new_fn = self.add_action_annotations(
let (maybe_new_fn, _) = self.add_action_annotations(
&f.ident,
&mut f.function,
Some(&mut f.function),
None,
is_exported,
is_default_export,
);
@ -427,6 +502,29 @@ impl<C: Comments> VisitMut for ServerActions<C> {
}
}
fn visit_mut_arrow_expr(&mut self, a: &mut ArrowExpr) {
// Arrow expressions need to be visited in prepass to determine if it's
// an action function or not.
if self.in_prepass {
let (is_action_fn, _, _) = self.get_action_info(
None,
if let BlockStmtOrExpr::BlockStmt(block) = &mut a.body {
Some(block)
} else {
None
},
);
// Store the span of the arrow expression if it's an action function.
if is_action_fn && a.is_async {
self.action_arrow_span.push(a.span);
}
return;
}
a.visit_mut_children_with(self);
}
fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
if self.in_action_file {
for decl in n.decls.iter_mut() {
@ -438,7 +536,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
if let Pat::Ident(ident) = &mut decl.name {
if let Some(fn_expr) = init.as_mut_fn_expr() {
// Collect `const foo = async function () {}` declarations. For now we
// ignore other types of assignments.
// just ignore other types of assignments.
if fn_expr.function.is_async {
if self.in_prepass {
self.async_fn_idents.push(ident.id.to_id());
@ -464,10 +562,6 @@ impl<C: Comments> VisitMut for ServerActions<C> {
}
}
}
if self.in_prepass {
return;
}
}
n.visit_mut_children_with(self);
@ -481,7 +575,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
fn visit_mut_stmt(&mut self, n: &mut Stmt) {
n.visit_mut_children_with(self);
if self.in_module || self.in_prepass {
if self.in_module {
return;
}
@ -610,7 +704,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
if let Some(init) = &decl.init {
match &**init {
Expr::Fn(_f) => {}
Expr::Arrow(_f) => {}
Expr::Arrow(_a) => {}
_ => {
disallowed_export_span = *span;
}
@ -652,21 +746,38 @@ impl<C: Comments> VisitMut for ServerActions<C> {
disallowed_export_span = *span;
}
},
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
expr,
span,
..
})) => match &**expr {
Expr::Fn(_f) => {}
Expr::Ident(ident) => {
if !self.async_fn_idents.contains(&ident.to_id()) {
disallowed_export_span = *span;
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(default_expr)) => {
match &mut *default_expr.expr {
Expr::Fn(_f) => {}
Expr::Arrow(a) => {
if !self.action_arrow_span.contains(&a.span) {
disallowed_export_span = default_expr.span;
} else {
// We need to give a name to the arrow function
// action and hoist it to the top.
let action_name: JsWord =
format!("$ACTION_default_{}", self.action_index).into();
self.action_index += 1;
let ident = Ident::new(action_name, DUMMY_SP);
self.add_action_annotations(&ident, None, Some(a), true, true);
default_expr.expr = Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP,
left: PatOrExpr::Pat(Box::new(Pat::Ident(ident.into()))),
op: op!("="),
right: Box::new(Expr::Arrow(a.clone())),
}));
}
}
Expr::Ident(ident) => {
if !self.async_fn_idents.contains(&ident.to_id()) {
disallowed_export_span = default_expr.span;
}
}
_ => {
disallowed_export_span = default_expr.span;
}
}
_ => {
disallowed_export_span = *span;
}
},
}
ModuleItem::ModuleDecl(ModuleDecl::ExportAll(ExportAll { span, .. })) => {
disallowed_export_span = *span;
}

View file

@ -0,0 +1,3 @@
'use server';
export default () => {}

View file

@ -0,0 +1 @@
/* __next_internal_action_entry_do_not_use__ */ export default (()=>{});

View file

@ -0,0 +1,7 @@
x Only async functions are allowed to be exported in a "use server" file.
,-[input.js:2:1]
2 |
3 | export default () => {}
: ^^^^^^^^^^^^^^^^^^^^^^^
`----

View file

@ -0,0 +1,3 @@
'use server'
export default async (a, b) => { console.log(a, b) }

View file

@ -0,0 +1,7 @@
/* __next_internal_action_entry_do_not_use__ default */ export default $ACTION_default_0 = async (a, b)=>{
console.log(a, b);
};
$ACTION_default_0.$$typeof = Symbol.for("react.server.reference");
$ACTION_default_0.$$id = "c18c215a6b7cdc64bf709f3a714ffdef1bf9651d";
$ACTION_default_0.$$bound = [];
var $ACTION_default_0;