Support named exports for server references (#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)
This commit is contained in:
parent
d167ecce47
commit
844776ef95
3 changed files with 146 additions and 16 deletions
|
@ -8,13 +8,7 @@ use next_binding::swc::core::{
|
|||
BytePos, FileName, DUMMY_SP,
|
||||
},
|
||||
ecma::{
|
||||
ast::{
|
||||
op, ArrayLit, AssignExpr, AssignPatProp, BlockStmt, CallExpr, ComputedPropName, Decl,
|
||||
DefaultDecl, ExportDecl, ExportDefaultDecl, Expr, ExprStmt, FnDecl, Function, Id,
|
||||
Ident, KeyValuePatProp, KeyValueProp, Lit, MemberExpr, MemberProp, Module, ModuleDecl,
|
||||
ModuleItem, ObjectPatProp, OptChainBase, OptChainExpr, Param, Pat, PatOrExpr, Prop,
|
||||
PropName, RestPat, ReturnStmt, Stmt, Str, VarDecl, VarDeclKind, VarDeclarator,
|
||||
},
|
||||
ast::*,
|
||||
atoms::JsWord,
|
||||
utils::{private_ident, quote_ident, ExprFactory},
|
||||
visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith},
|
||||
|
@ -40,6 +34,7 @@ pub fn server_actions<C: Comments>(
|
|||
start_pos: BytePos(0),
|
||||
in_action_file: false,
|
||||
in_export_decl: false,
|
||||
in_prepass: false,
|
||||
has_action: false,
|
||||
top_level: false,
|
||||
|
||||
|
@ -48,6 +43,8 @@ pub fn server_actions<C: Comments>(
|
|||
should_add_name: false,
|
||||
closure_idents: Default::default(),
|
||||
action_idents: Default::default(),
|
||||
async_fn_idents: Default::default(),
|
||||
exported_idents: Default::default(),
|
||||
|
||||
annotations: Default::default(),
|
||||
extra_items: Default::default(),
|
||||
|
@ -64,6 +61,7 @@ struct ServerActions<C: Comments> {
|
|||
start_pos: BytePos,
|
||||
in_action_file: bool,
|
||||
in_export_decl: bool,
|
||||
in_prepass: bool,
|
||||
has_action: bool,
|
||||
top_level: bool,
|
||||
|
||||
|
@ -72,6 +70,8 @@ struct ServerActions<C: Comments> {
|
|||
should_add_name: bool,
|
||||
closure_idents: Vec<Id>,
|
||||
action_idents: Vec<Name>,
|
||||
async_fn_idents: Vec<Id>,
|
||||
exported_idents: Vec<Id>,
|
||||
|
||||
annotations: Vec<Stmt>,
|
||||
extra_items: Vec<ModuleItem>,
|
||||
|
@ -87,20 +87,36 @@ impl<C: Comments> VisitMut for ServerActions<C> {
|
|||
}
|
||||
|
||||
fn visit_mut_fn_decl(&mut self, f: &mut FnDecl) {
|
||||
let mut in_action_fn = false;
|
||||
// Need to collect all async function identifiers if we are in a server
|
||||
// file, because it can be exported later.
|
||||
if self.in_action_file && self.in_prepass {
|
||||
if f.function.is_async {
|
||||
self.async_fn_idents.push(f.ident.to_id());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut is_action_fn = false;
|
||||
let mut is_exported = false;
|
||||
|
||||
if self.in_action_file && self.in_export_decl {
|
||||
// All export functions in a server file are actions
|
||||
in_action_fn = true;
|
||||
is_action_fn = true;
|
||||
} else {
|
||||
// Check if the function has `"use server"`
|
||||
if let Some(body) = &mut f.function.body {
|
||||
let directive_index = get_server_directive_index_in_fn(&body.stmts);
|
||||
if directive_index >= 0 {
|
||||
in_action_fn = true;
|
||||
is_action_fn = true;
|
||||
body.stmts.remove(directive_index.try_into().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// If it's exported via named export, it's a valid action.
|
||||
if !is_action_fn && self.exported_idents.contains(&f.ident.to_id()) {
|
||||
is_action_fn = true;
|
||||
is_exported = true;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -108,7 +124,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
|
|||
let old_in_action_fn = self.in_action_fn;
|
||||
let old_in_module = self.in_module;
|
||||
let old_should_add_name = self.should_add_name;
|
||||
self.in_action_fn = in_action_fn;
|
||||
self.in_action_fn = is_action_fn;
|
||||
self.in_module = false;
|
||||
self.should_add_name = true;
|
||||
f.visit_mut_children_with(self);
|
||||
|
@ -117,7 +133,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
|
|||
self.should_add_name = old_should_add_name;
|
||||
}
|
||||
|
||||
if !in_action_fn {
|
||||
if !is_action_fn {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -129,7 +145,8 @@ impl<C: Comments> VisitMut for ServerActions<C> {
|
|||
});
|
||||
}
|
||||
|
||||
let action_name: JsWord = if self.in_action_file && self.in_export_decl {
|
||||
let need_rename_export = self.in_action_file && (self.in_export_decl || is_exported);
|
||||
let action_name: JsWord = if need_rename_export {
|
||||
f.ident.sym.clone()
|
||||
} else {
|
||||
format!("$ACTION_{}", f.ident.sym).into()
|
||||
|
@ -177,7 +194,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
|
|||
.into(),
|
||||
));
|
||||
|
||||
if !(self.in_action_file && self.in_export_decl) {
|
||||
if !need_rename_export {
|
||||
// export const $ACTION_myAction = myAction;
|
||||
self.extra_items
|
||||
.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
|
||||
|
@ -277,7 +294,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 {
|
||||
if self.in_module || self.in_prepass {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -290,6 +307,10 @@ impl<C: Comments> VisitMut for ServerActions<C> {
|
|||
fn visit_mut_param(&mut self, n: &mut Param) {
|
||||
n.visit_mut_children_with(self);
|
||||
|
||||
if self.in_prepass {
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.in_action_fn && !self.in_action_file {
|
||||
match &n.pat {
|
||||
Pat::Ident(ident) => {
|
||||
|
@ -317,7 +338,9 @@ impl<C: Comments> VisitMut for ServerActions<C> {
|
|||
if self.in_action_fn && self.should_add_name {
|
||||
if let Ok(name) = Name::try_from(&*n) {
|
||||
self.should_add_name = false;
|
||||
if !self.in_prepass {
|
||||
self.action_idents.push(name);
|
||||
}
|
||||
n.visit_mut_children_with(self);
|
||||
self.should_add_name = true;
|
||||
return;
|
||||
|
@ -338,6 +361,40 @@ impl<C: Comments> VisitMut for ServerActions<C> {
|
|||
let old_annotations = self.annotations.take();
|
||||
|
||||
let mut new = Vec::with_capacity(stmts.len());
|
||||
|
||||
// We need a second pass to collect all async function idents and exports
|
||||
// so we can handle the named export cases if it's in the "use server" file.
|
||||
if self.in_action_file {
|
||||
self.in_prepass = true;
|
||||
for stmt in stmts.iter_mut() {
|
||||
match &*stmt {
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
|
||||
decl: Decl::Var(var),
|
||||
..
|
||||
})) => {
|
||||
let ids: Vec<Id> = collect_idents_in_var_decls(&var.decls);
|
||||
self.exported_idents.extend(ids);
|
||||
}
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) => {
|
||||
for spec in &named.specifiers {
|
||||
if let ExportSpecifier::Named(ExportNamedSpecifier {
|
||||
orig: ModuleExportName::Ident(ident),
|
||||
..
|
||||
}) = spec
|
||||
{
|
||||
// export { foo, foo as bar }
|
||||
self.exported_idents.push(ident.to_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
stmt.visit_mut_with(self);
|
||||
}
|
||||
self.in_prepass = false;
|
||||
}
|
||||
|
||||
for mut stmt in stmts.take() {
|
||||
self.top_level = true;
|
||||
|
||||
|
@ -345,15 +402,48 @@ impl<C: Comments> VisitMut for ServerActions<C> {
|
|||
// functions.
|
||||
if self.in_action_file {
|
||||
let mut disallowed_export_span = DUMMY_SP;
|
||||
|
||||
// Currrently only function exports are allowed.
|
||||
match &mut stmt {
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, span })) => {
|
||||
match decl {
|
||||
Decl::Fn(_f) => {}
|
||||
Decl::Var(var) => {
|
||||
for decl in &mut var.decls {
|
||||
if let Some(init) = &decl.init {
|
||||
match &**init {
|
||||
Expr::Fn(_f) => {}
|
||||
_ => {
|
||||
disallowed_export_span = *span;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
disallowed_export_span = *span;
|
||||
}
|
||||
}
|
||||
}
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) => {
|
||||
if named.src.is_some() {
|
||||
disallowed_export_span = named.span;
|
||||
} else {
|
||||
for spec in &mut named.specifiers {
|
||||
if let ExportSpecifier::Named(ExportNamedSpecifier {
|
||||
orig: ModuleExportName::Ident(ident),
|
||||
..
|
||||
}) = spec
|
||||
{
|
||||
if !self.async_fn_idents.contains(&ident.to_id()) {
|
||||
disallowed_export_span = named.span;
|
||||
}
|
||||
} else {
|
||||
disallowed_export_span = named.span;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
|
||||
decl,
|
||||
span,
|
||||
|
@ -364,6 +454,16 @@ impl<C: Comments> VisitMut for ServerActions<C> {
|
|||
disallowed_export_span = *span;
|
||||
}
|
||||
},
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
|
||||
expr,
|
||||
span,
|
||||
..
|
||||
})) => match &**expr {
|
||||
Expr::Fn(_f) => {}
|
||||
_ => {
|
||||
disallowed_export_span = *span;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
// app/send.ts
|
||||
"use server";
|
||||
|
||||
async function foo () {}
|
||||
export { foo }
|
||||
|
||||
async function bar() {}
|
||||
export { bar as baz }
|
||||
|
||||
async function qux() {}
|
||||
export { qux as default }
|
|
@ -0,0 +1,19 @@
|
|||
// app/send.ts
|
||||
/* __next_internal_action_entry_do_not_use__ foo,bar,qux */ async function foo() {}
|
||||
foo.$$typeof = Symbol.for("react.server.reference");
|
||||
foo.$$filepath = "/app/item.js";
|
||||
foo.$$name = "foo";
|
||||
foo.$$bound = [];
|
||||
export { foo };
|
||||
async function bar() {}
|
||||
bar.$$typeof = Symbol.for("react.server.reference");
|
||||
bar.$$filepath = "/app/item.js";
|
||||
bar.$$name = "bar";
|
||||
bar.$$bound = [];
|
||||
export { bar as baz };
|
||||
async function qux() {}
|
||||
qux.$$typeof = Symbol.for("react.server.reference");
|
||||
qux.$$filepath = "/app/item.js";
|
||||
qux.$$name = "qux";
|
||||
qux.$$bound = [];
|
||||
export { qux as default };
|
Loading…
Reference in a new issue