Skip to content

Commit

Permalink
Fix server component transforms (#49135)
Browse files Browse the repository at this point in the history
This makes sure that we are treating a module as in the server layer, if
it's under the client layer but with file-level `"use server"`. Also
makes sure we're catching errors like having both `"use server"` and
`"use client"` directives presented.
  • Loading branch information
shuding committed May 3, 2023
1 parent e659653 commit d957327
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 6 deletions.
35 changes: 31 additions & 4 deletions packages/next-swc/crates/core/src/react_server_components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ impl<C: Comments> VisitMut for ReactServerComponents<C> {
noop_visit_mut_type!();

fn visit_mut_module(&mut self, module: &mut Module) {
let (is_client_entry, imports) = self.collect_top_level_directives_and_imports(module);
let (is_client_entry, is_action_file, imports) =
self.collect_top_level_directives_and_imports(module);
let is_cjs = contains_cjs(module);

if self.is_server {
Expand All @@ -72,7 +73,9 @@ impl<C: Comments> VisitMut for ReactServerComponents<C> {
return;
}
} else {
self.assert_client_graph(&imports, module);
if !is_action_file {
self.assert_client_graph(&imports, module);
}
if is_client_entry {
self.prepend_comment_node(module, is_cjs);
}
Expand All @@ -87,10 +90,24 @@ impl<C: Comments> ReactServerComponents<C> {
fn collect_top_level_directives_and_imports(
&mut self,
module: &mut Module,
) -> (bool, Vec<ModuleImports>) {
) -> (bool, bool, Vec<ModuleImports>) {
let mut imports: Vec<ModuleImports> = vec![];
let mut finished_directives = false;
let mut is_client_entry = false;
let mut is_action_file = false;

fn panic_both_directives(span: Span) {
// It's not possible to have both directives in the same file.
HANDLER.with(|handler| {
handler
.struct_span_err(
span,
"It's not possible to have both `use client` and `use server` directives \
in the same file.",
)
.emit()
})
}

let _ = &module.body.retain(|item| {
match item {
Expand All @@ -107,6 +124,10 @@ impl<C: Comments> ReactServerComponents<C> {
if &**value == "use client" {
if !finished_directives {
is_client_entry = true;

if is_action_file {
panic_both_directives(expr_stmt.span)
}
} else {
HANDLER.with(|handler| {
handler
Expand All @@ -120,6 +141,12 @@ impl<C: Comments> ReactServerComponents<C> {

// Remove the directive.
return false;
} else if &**value == "use server" && !finished_directives {
is_action_file = true;

if is_client_entry {
panic_both_directives(expr_stmt.span)
}
}
}
// Match `ParenthesisExpression` which is some formatting tools
Expand Down Expand Up @@ -230,7 +257,7 @@ impl<C: Comments> ReactServerComponents<C> {
true
});

(is_client_entry, imports)
(is_client_entry, is_action_file, imports)
}

// Convert the client module to the module reference code and add a special
Expand Down
12 changes: 10 additions & 2 deletions packages/next-swc/crates/core/tests/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,21 @@ fn react_server_actions_errors(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
test_fixture(
syntax(),
&|_tr| {
&|tr| {
chain!(
resolver(Mark::new(), Mark::new(), false),
server_components(
FileName::Real(PathBuf::from("/app/item.js")),
next_swc::react_server_components::Config::WithOptions(
next_swc::react_server_components::Options { is_server: false },
),
tr.comments.as_ref().clone(),
None,
),
server_actions(
&FileName::Real("/app/item.js".into()),
server_actions::Config { is_server: true },
_tr.comments.as_ref().clone(),
tr.comments.as_ref().clone(),
)
)
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use server'
'use client'

export async function foo() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* __next_internal_client_entry_do_not_use__ foo auto */ /* __next_internal_action_entry_do_not_use__ foo */ export async function foo() {}
import ensureServerEntryExports from "private-next-rsc-action-proxy";
ensureServerEntryExports([
foo
]);
foo.$$typeof = Symbol.for("react.server.reference");
foo.$$id = "ab21efdafbe611287bc25c0462b1e0510d13e48b";
foo.$$bound = [];
foo.$$with_bound = false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

x It's not possible to have both `use client` and `use server` directives in the same file.
,-[input.js:1:1]
1 | 'use server'
2 | 'use client'
: ^^^^^^^^^^^^
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'
'use strict'

// This is a comment.

'use server'

export async function foo() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* __next_internal_client_entry_do_not_use__ foo auto */ /* __next_internal_action_entry_do_not_use__ foo */ 'use strict';
export async function foo() {}
import ensureServerEntryExports from "private-next-rsc-action-proxy";
ensureServerEntryExports([
foo
]);
foo.$$typeof = Symbol.for("react.server.reference");
foo.$$id = "ab21efdafbe611287bc25c0462b1e0510d13e48b";
foo.$$bound = [];
foo.$$with_bound = false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

x It's not possible to have both `use client` and `use server` directives in the same file.
,-[input.js:5:1]
5 |
6 | 'use server'
: ^^^^^^^^^^^^
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use server'

// It should be allowed to import server APIs here.
import 'server-only'
import { cookies } from 'next/headers'
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use server';
// It should be allowed to import server APIs here.
import 'server-only';
import { cookies } from 'next/headers';

0 comments on commit d957327

Please sign in to comment.