Skip to content

Commit 844776e

Browse files
authored
Support named exports for server references (vercel#46558)
NEXT-424 Note that this change also prepares for upcoming PRs to support arrow functions. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
1 parent d167ecc commit 844776e

File tree

3 files changed

+146
-16
lines changed

3 files changed

+146
-16
lines changed

packages/next-swc/crates/core/src/server_actions.rs

+116-16
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@ use next_binding::swc::core::{
88
BytePos, FileName, DUMMY_SP,
99
},
1010
ecma::{
11-
ast::{
12-
op, ArrayLit, AssignExpr, AssignPatProp, BlockStmt, CallExpr, ComputedPropName, Decl,
13-
DefaultDecl, ExportDecl, ExportDefaultDecl, Expr, ExprStmt, FnDecl, Function, Id,
14-
Ident, KeyValuePatProp, KeyValueProp, Lit, MemberExpr, MemberProp, Module, ModuleDecl,
15-
ModuleItem, ObjectPatProp, OptChainBase, OptChainExpr, Param, Pat, PatOrExpr, Prop,
16-
PropName, RestPat, ReturnStmt, Stmt, Str, VarDecl, VarDeclKind, VarDeclarator,
17-
},
11+
ast::*,
1812
atoms::JsWord,
1913
utils::{private_ident, quote_ident, ExprFactory},
2014
visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith},
@@ -40,6 +34,7 @@ pub fn server_actions<C: Comments>(
4034
start_pos: BytePos(0),
4135
in_action_file: false,
4236
in_export_decl: false,
37+
in_prepass: false,
4338
has_action: false,
4439
top_level: false,
4540

@@ -48,6 +43,8 @@ pub fn server_actions<C: Comments>(
4843
should_add_name: false,
4944
closure_idents: Default::default(),
5045
action_idents: Default::default(),
46+
async_fn_idents: Default::default(),
47+
exported_idents: Default::default(),
5148

5249
annotations: Default::default(),
5350
extra_items: Default::default(),
@@ -64,6 +61,7 @@ struct ServerActions<C: Comments> {
6461
start_pos: BytePos,
6562
in_action_file: bool,
6663
in_export_decl: bool,
64+
in_prepass: bool,
6765
has_action: bool,
6866
top_level: bool,
6967

@@ -72,6 +70,8 @@ struct ServerActions<C: Comments> {
7270
should_add_name: bool,
7371
closure_idents: Vec<Id>,
7472
action_idents: Vec<Name>,
73+
async_fn_idents: Vec<Id>,
74+
exported_idents: Vec<Id>,
7575

7676
annotations: Vec<Stmt>,
7777
extra_items: Vec<ModuleItem>,
@@ -87,28 +87,44 @@ impl<C: Comments> VisitMut for ServerActions<C> {
8787
}
8888

8989
fn visit_mut_fn_decl(&mut self, f: &mut FnDecl) {
90-
let mut in_action_fn = false;
90+
// Need to collect all async function identifiers if we are in a server
91+
// file, because it can be exported later.
92+
if self.in_action_file && self.in_prepass {
93+
if f.function.is_async {
94+
self.async_fn_idents.push(f.ident.to_id());
95+
}
96+
return;
97+
}
98+
99+
let mut is_action_fn = false;
100+
let mut is_exported = false;
91101

92102
if self.in_action_file && self.in_export_decl {
93103
// All export functions in a server file are actions
94-
in_action_fn = true;
104+
is_action_fn = true;
95105
} else {
96106
// Check if the function has `"use server"`
97107
if let Some(body) = &mut f.function.body {
98108
let directive_index = get_server_directive_index_in_fn(&body.stmts);
99109
if directive_index >= 0 {
100-
in_action_fn = true;
110+
is_action_fn = true;
101111
body.stmts.remove(directive_index.try_into().unwrap());
102112
}
103113
}
114+
115+
// If it's exported via named export, it's a valid action.
116+
if !is_action_fn && self.exported_idents.contains(&f.ident.to_id()) {
117+
is_action_fn = true;
118+
is_exported = true;
119+
}
104120
}
105121

106122
{
107123
// Visit children
108124
let old_in_action_fn = self.in_action_fn;
109125
let old_in_module = self.in_module;
110126
let old_should_add_name = self.should_add_name;
111-
self.in_action_fn = in_action_fn;
127+
self.in_action_fn = is_action_fn;
112128
self.in_module = false;
113129
self.should_add_name = true;
114130
f.visit_mut_children_with(self);
@@ -117,7 +133,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
117133
self.should_add_name = old_should_add_name;
118134
}
119135

120-
if !in_action_fn {
136+
if !is_action_fn {
121137
return;
122138
}
123139

@@ -129,7 +145,8 @@ impl<C: Comments> VisitMut for ServerActions<C> {
129145
});
130146
}
131147

132-
let action_name: JsWord = if self.in_action_file && self.in_export_decl {
148+
let need_rename_export = self.in_action_file && (self.in_export_decl || is_exported);
149+
let action_name: JsWord = if need_rename_export {
133150
f.ident.sym.clone()
134151
} else {
135152
format!("$ACTION_{}", f.ident.sym).into()
@@ -177,7 +194,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
177194
.into(),
178195
));
179196

180-
if !(self.in_action_file && self.in_export_decl) {
197+
if !need_rename_export {
181198
// export const $ACTION_myAction = myAction;
182199
self.extra_items
183200
.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
@@ -277,7 +294,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
277294
fn visit_mut_stmt(&mut self, n: &mut Stmt) {
278295
n.visit_mut_children_with(self);
279296

280-
if self.in_module {
297+
if self.in_module || self.in_prepass {
281298
return;
282299
}
283300

@@ -290,6 +307,10 @@ impl<C: Comments> VisitMut for ServerActions<C> {
290307
fn visit_mut_param(&mut self, n: &mut Param) {
291308
n.visit_mut_children_with(self);
292309

310+
if self.in_prepass {
311+
return;
312+
}
313+
293314
if !self.in_action_fn && !self.in_action_file {
294315
match &n.pat {
295316
Pat::Ident(ident) => {
@@ -317,7 +338,9 @@ impl<C: Comments> VisitMut for ServerActions<C> {
317338
if self.in_action_fn && self.should_add_name {
318339
if let Ok(name) = Name::try_from(&*n) {
319340
self.should_add_name = false;
320-
self.action_idents.push(name);
341+
if !self.in_prepass {
342+
self.action_idents.push(name);
343+
}
321344
n.visit_mut_children_with(self);
322345
self.should_add_name = true;
323346
return;
@@ -338,22 +361,89 @@ impl<C: Comments> VisitMut for ServerActions<C> {
338361
let old_annotations = self.annotations.take();
339362

340363
let mut new = Vec::with_capacity(stmts.len());
364+
365+
// We need a second pass to collect all async function idents and exports
366+
// so we can handle the named export cases if it's in the "use server" file.
367+
if self.in_action_file {
368+
self.in_prepass = true;
369+
for stmt in stmts.iter_mut() {
370+
match &*stmt {
371+
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
372+
decl: Decl::Var(var),
373+
..
374+
})) => {
375+
let ids: Vec<Id> = collect_idents_in_var_decls(&var.decls);
376+
self.exported_idents.extend(ids);
377+
}
378+
ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) => {
379+
for spec in &named.specifiers {
380+
if let ExportSpecifier::Named(ExportNamedSpecifier {
381+
orig: ModuleExportName::Ident(ident),
382+
..
383+
}) = spec
384+
{
385+
// export { foo, foo as bar }
386+
self.exported_idents.push(ident.to_id());
387+
}
388+
}
389+
}
390+
_ => {}
391+
}
392+
393+
stmt.visit_mut_with(self);
394+
}
395+
self.in_prepass = false;
396+
}
397+
341398
for mut stmt in stmts.take() {
342399
self.top_level = true;
343400

344401
// For action file, it's not allowed to export things other than async
345402
// functions.
346403
if self.in_action_file {
347404
let mut disallowed_export_span = DUMMY_SP;
405+
406+
// Currrently only function exports are allowed.
348407
match &mut stmt {
349408
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, span })) => {
350409
match decl {
351410
Decl::Fn(_f) => {}
411+
Decl::Var(var) => {
412+
for decl in &mut var.decls {
413+
if let Some(init) = &decl.init {
414+
match &**init {
415+
Expr::Fn(_f) => {}
416+
_ => {
417+
disallowed_export_span = *span;
418+
}
419+
}
420+
}
421+
}
422+
}
352423
_ => {
353424
disallowed_export_span = *span;
354425
}
355426
}
356427
}
428+
ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) => {
429+
if named.src.is_some() {
430+
disallowed_export_span = named.span;
431+
} else {
432+
for spec in &mut named.specifiers {
433+
if let ExportSpecifier::Named(ExportNamedSpecifier {
434+
orig: ModuleExportName::Ident(ident),
435+
..
436+
}) = spec
437+
{
438+
if !self.async_fn_idents.contains(&ident.to_id()) {
439+
disallowed_export_span = named.span;
440+
}
441+
} else {
442+
disallowed_export_span = named.span;
443+
}
444+
}
445+
}
446+
}
357447
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
358448
decl,
359449
span,
@@ -364,6 +454,16 @@ impl<C: Comments> VisitMut for ServerActions<C> {
364454
disallowed_export_span = *span;
365455
}
366456
},
457+
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
458+
expr,
459+
span,
460+
..
461+
})) => match &**expr {
462+
Expr::Fn(_f) => {}
463+
_ => {
464+
disallowed_export_span = *span;
465+
}
466+
},
367467
_ => {}
368468
}
369469

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// app/send.ts
2+
"use server";
3+
4+
async function foo () {}
5+
export { foo }
6+
7+
async function bar() {}
8+
export { bar as baz }
9+
10+
async function qux() {}
11+
export { qux as default }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// app/send.ts
2+
/* __next_internal_action_entry_do_not_use__ foo,bar,qux */ async function foo() {}
3+
foo.$$typeof = Symbol.for("react.server.reference");
4+
foo.$$filepath = "/app/item.js";
5+
foo.$$name = "foo";
6+
foo.$$bound = [];
7+
export { foo };
8+
async function bar() {}
9+
bar.$$typeof = Symbol.for("react.server.reference");
10+
bar.$$filepath = "/app/item.js";
11+
bar.$$name = "bar";
12+
bar.$$bound = [];
13+
export { bar as baz };
14+
async function qux() {}
15+
qux.$$typeof = Symbol.for("react.server.reference");
16+
qux.$$filepath = "/app/item.js";
17+
qux.$$name = "qux";
18+
qux.$$bound = [];
19+
export { qux as default };

0 commit comments

Comments
 (0)