Skip to content

Commit 4c3f59f

Browse files
authored
Implement server entry creation in client layer (#47127
In short, this PR adds a 3rd layer to the server compiler. This extra layer is for marking the modules when re-entering the server layer from a client component. It is almost identical to the existing server layer and it should have all the same bundling and runtime behaviors, but it's still special because it's not allowed to enter the client layer again from there. Because of that, we create the extra entry for that new layer when the client layer compilation finishes in the `finishModules` phase. The new entry is handled normally as it's in the server layer. But the original module in the client layer will be compiled specially as special no-op exports, and will then be connected via the `callServer` wrapper. fix NEXT-809 ([link](https://linear.app/vercel/issue/NEXT-809)).
1 parent 02125cf commit 4c3f59f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+469
-119
lines changed

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

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ impl<C: Comments> ServerActions<C> {
102102
remove_directive,
103103
&mut is_action_fn,
104104
);
105+
106+
if is_action_fn && !self.config.is_server {
107+
HANDLER.with(|handler| {
108+
handler
109+
.struct_span_err(
110+
body.span,
111+
"\"use server\" functions are not allowed in client components. \
112+
You can import them from a \"use server\" file instead.",
113+
)
114+
.emit()
115+
});
116+
}
105117
}
106118
}
107119

@@ -692,9 +704,11 @@ impl<C: Comments> VisitMut for ServerActions<C> {
692704

693705
stmt.visit_mut_with(self);
694706

695-
new.push(stmt);
696-
new.extend(self.annotations.drain(..).map(ModuleItem::Stmt));
697-
new.append(&mut self.extra_items);
707+
if self.config.is_server || !self.in_action_file {
708+
new.push(stmt);
709+
new.extend(self.annotations.drain(..).map(ModuleItem::Stmt));
710+
new.append(&mut self.extra_items);
711+
}
698712
}
699713

700714
// If it's a "use server" file, all exports need to be annotated as actions.
@@ -703,11 +717,70 @@ impl<C: Comments> VisitMut for ServerActions<C> {
703717
let ident = Ident::new(id.0.clone(), DUMMY_SP.with_ctxt(id.1));
704718
annotate_ident_as_action(
705719
&mut self.annotations,
706-
ident,
720+
ident.clone(),
707721
Vec::new(),
708722
self.file_name.to_string(),
709723
export_name.to_string(),
710724
);
725+
if !self.config.is_server {
726+
let params_ident = private_ident!("args");
727+
let noop_fn = Box::new(Function {
728+
params: vec![Param {
729+
span: DUMMY_SP,
730+
decorators: Default::default(),
731+
pat: Pat::Rest(RestPat {
732+
span: DUMMY_SP,
733+
dot3_token: DUMMY_SP,
734+
arg: Box::new(Pat::Ident(params_ident.clone().into())),
735+
type_ann: None,
736+
}),
737+
}],
738+
decorators: Vec::new(),
739+
span: DUMMY_SP,
740+
body: Some(BlockStmt {
741+
span: DUMMY_SP,
742+
stmts: vec![Stmt::Return(ReturnStmt {
743+
span: DUMMY_SP,
744+
arg: Some(Box::new(Expr::Call(CallExpr {
745+
span: DUMMY_SP,
746+
callee: Callee::Expr(Box::new(Expr::Ident(private_ident!(
747+
"__build_action__"
748+
)))),
749+
args: vec![ident.clone().as_arg(), params_ident.as_arg()],
750+
type_args: None,
751+
}))),
752+
})],
753+
}),
754+
is_generator: false,
755+
is_async: true,
756+
type_params: None,
757+
return_type: None,
758+
});
759+
760+
if export_name == "default" {
761+
let export_expr = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(
762+
ExportDefaultExpr {
763+
span: DUMMY_SP,
764+
expr: Box::new(Expr::Fn(FnExpr {
765+
ident: Some(ident),
766+
function: noop_fn,
767+
})),
768+
},
769+
));
770+
new.push(export_expr);
771+
} else {
772+
let export_expr =
773+
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
774+
span: DUMMY_SP,
775+
decl: Decl::Fn(FnDecl {
776+
ident,
777+
declare: false,
778+
function: noop_fn,
779+
}),
780+
}));
781+
new.push(export_expr);
782+
}
783+
}
711784
}
712785
new.extend(self.annotations.drain(..).map(ModuleItem::Stmt));
713786
new.append(&mut self.extra_items);

packages/next-swc/crates/core/tests/fixture.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,8 @@ fn next_font_loaders_fixture(input: PathBuf) {
301301
);
302302
}
303303

304-
#[fixture("tests/fixture/server-actions/**/input.js")]
305-
fn server_actions_fixture(input: PathBuf) {
304+
#[fixture("tests/fixture/server-actions/server/**/input.js")]
305+
fn server_actions_server_fixture(input: PathBuf) {
306306
let output = input.parent().unwrap().join("output.js");
307307
test_fixture(
308308
syntax(),
@@ -321,3 +321,24 @@ fn server_actions_fixture(input: PathBuf) {
321321
Default::default(),
322322
);
323323
}
324+
325+
#[fixture("tests/fixture/server-actions/client/**/input.js")]
326+
fn server_actions_client_fixture(input: PathBuf) {
327+
let output = input.parent().unwrap().join("output.js");
328+
test_fixture(
329+
syntax(),
330+
&|_tr| {
331+
chain!(
332+
resolver(Mark::new(), Mark::new(), false),
333+
server_actions(
334+
&FileName::Real("/app/item.js".into()),
335+
server_actions::Config { is_server: false },
336+
_tr.comments.as_ref().clone(),
337+
)
338+
)
339+
},
340+
&input,
341+
&output,
342+
Default::default(),
343+
);
344+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// app/send.ts
2+
"use server";
3+
export async function myAction(a, b, c) {
4+
console.log('a')
5+
}
6+
export default async function () {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// app/send.ts
2+
/* __next_internal_action_entry_do_not_use__ myAction,default */ export async function myAction(...args) {
3+
return __build_action__(myAction, args);
4+
}
5+
export default async function $$ACTION_0(...args) {
6+
return __build_action__($$ACTION_0, args);
7+
};
8+
myAction.$$typeof = Symbol.for("react.server.reference");
9+
myAction.$$id = "e10665baac148856374b2789aceb970f66fec33e";
10+
myAction.$$bound = [];
11+
$$ACTION_0.$$typeof = Symbol.for("react.server.reference");
12+
$$ACTION_0.$$id = "c18c215a6b7cdc64bf709f3a714ffdef1bf9651d";
13+
$$ACTION_0.$$bound = [];
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// app/send.ts
2+
"use server";
3+
4+
import 'db'
5+
6+
console.log('side effect')
7+
8+
const foo = async () => {
9+
console.log('function body')
10+
}
11+
export { foo }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// app/send.ts
2+
/* __next_internal_action_entry_do_not_use__ foo */ export async function foo(...args) {
3+
return __build_action__(foo, args);
4+
}
5+
foo.$$typeof = Symbol.for("react.server.reference");
6+
foo.$$id = "ab21efdafbe611287bc25c0462b1e0510d13e48b";
7+
foo.$$bound = [];

0 commit comments

Comments
 (0)