Skip to content
Merged
81 changes: 77 additions & 4 deletions packages/next-swc/crates/core/src/server_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ impl<C: Comments> ServerActions<C> {
remove_directive,
&mut is_action_fn,
);

if is_action_fn && !self.config.is_server {
HANDLER.with(|handler| {
handler
.struct_span_err(
body.span,
"\"use server\" functions are not allowed in client components. \
You can import them from a \"use server\" file instead.",
)
.emit()
});
}
}
}

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

stmt.visit_mut_with(self);

new.push(stmt);
new.extend(self.annotations.drain(..).map(ModuleItem::Stmt));
new.append(&mut self.extra_items);
if self.config.is_server || !self.in_action_file {
new.push(stmt);
new.extend(self.annotations.drain(..).map(ModuleItem::Stmt));
new.append(&mut self.extra_items);
}
}

// If it's a "use server" file, all exports need to be annotated as actions.
Expand All @@ -703,11 +717,70 @@ impl<C: Comments> VisitMut for ServerActions<C> {
let ident = Ident::new(id.0.clone(), DUMMY_SP.with_ctxt(id.1));
annotate_ident_as_action(
&mut self.annotations,
ident,
ident.clone(),
Vec::new(),
self.file_name.to_string(),
export_name.to_string(),
);
if !self.config.is_server {
let params_ident = private_ident!("args");
let noop_fn = Box::new(Function {
params: vec![Param {
span: DUMMY_SP,
decorators: Default::default(),
pat: Pat::Rest(RestPat {
span: DUMMY_SP,
dot3_token: DUMMY_SP,
arg: Box::new(Pat::Ident(params_ident.clone().into())),
type_ann: None,
}),
}],
decorators: Vec::new(),
span: DUMMY_SP,
body: Some(BlockStmt {
span: DUMMY_SP,
stmts: vec![Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(Box::new(Expr::Call(CallExpr {
span: DUMMY_SP,
callee: Callee::Expr(Box::new(Expr::Ident(private_ident!(
"__build_action__"
)))),
args: vec![ident.clone().as_arg(), params_ident.as_arg()],
type_args: None,
}))),
})],
}),
is_generator: false,
is_async: true,
type_params: None,
return_type: None,
});

if export_name == "default" {
let export_expr = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(
ExportDefaultExpr {
span: DUMMY_SP,
expr: Box::new(Expr::Fn(FnExpr {
ident: Some(ident),
function: noop_fn,
})),
},
));
new.push(export_expr);
} else {
let export_expr =
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
span: DUMMY_SP,
decl: Decl::Fn(FnDecl {
ident,
declare: false,
function: noop_fn,
}),
}));
new.push(export_expr);
}
}
}
new.extend(self.annotations.drain(..).map(ModuleItem::Stmt));
new.append(&mut self.extra_items);
Expand Down
25 changes: 23 additions & 2 deletions packages/next-swc/crates/core/tests/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ fn next_font_loaders_fixture(input: PathBuf) {
);
}

#[fixture("tests/fixture/server-actions/**/input.js")]
fn server_actions_fixture(input: PathBuf) {
#[fixture("tests/fixture/server-actions/server/**/input.js")]
fn server_actions_server_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
test_fixture(
syntax(),
Expand All @@ -321,3 +321,24 @@ fn server_actions_fixture(input: PathBuf) {
Default::default(),
);
}

#[fixture("tests/fixture/server-actions/client/**/input.js")]
fn server_actions_client_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
test_fixture(
syntax(),
&|_tr| {
chain!(
resolver(Mark::new(), Mark::new(), false),
server_actions(
&FileName::Real("/app/item.js".into()),
server_actions::Config { is_server: false },
_tr.comments.as_ref().clone(),
)
)
},
&input,
&output,
Default::default(),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// app/send.ts
"use server";
export async function myAction(a, b, c) {
console.log('a')
}
export default async function () {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// app/send.ts
/* __next_internal_action_entry_do_not_use__ myAction,default */ export async function myAction(...args) {
return __build_action__(myAction, args);
}
export default async function $$ACTION_0(...args) {
return __build_action__($$ACTION_0, args);
};
myAction.$$typeof = Symbol.for("react.server.reference");
myAction.$$id = "e10665baac148856374b2789aceb970f66fec33e";
myAction.$$bound = [];
$$ACTION_0.$$typeof = Symbol.for("react.server.reference");
$$ACTION_0.$$id = "c18c215a6b7cdc64bf709f3a714ffdef1bf9651d";
$$ACTION_0.$$bound = [];
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// app/send.ts
"use server";

import 'db'

console.log('side effect')

const foo = async () => {
console.log('function body')
}
export { foo }
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// app/send.ts
/* __next_internal_action_entry_do_not_use__ foo */ export async function foo(...args) {
return __build_action__(foo, args);
}
foo.$$typeof = Symbol.for("react.server.reference");
foo.$$id = "ab21efdafbe611287bc25c0462b1e0510d13e48b";
foo.$$bound = [];
15 changes: 13 additions & 2 deletions packages/next/src/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,21 @@ const ACTION_MODULE_LABEL =
/\/\* __next_internal_action_entry_do_not_use__ ([^ ]+) \*\//

export type RSCModuleType = 'server' | 'client'
export function getRSCModuleInformation(source: string): RSCMeta {
const clientRefs = source.match(CLIENT_MODULE_LABEL)?.[1]?.split(',')
export function getRSCModuleInformation(
source: string,
isServerLayer = true
): RSCMeta {
const actions = source.match(ACTION_MODULE_LABEL)?.[1]?.split(',')

if (!isServerLayer) {
return {
type: RSC_MODULE_TYPES.client,
actions,
}
}

const clientRefs = source.match(CLIENT_MODULE_LABEL)?.[1]?.split(',')

const type = clientRefs ? RSC_MODULE_TYPES.client : RSC_MODULE_TYPES.server
return { type, actions, clientRefs }
}
Expand Down
50 changes: 45 additions & 5 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,13 +786,22 @@ export default async function getBaseWebpackConfig(

const swcLoaderForServerLayer = hasServerComponents
? useSWCLoader
? getSwcLoader({ isServerLayer: true })
? [getSwcLoader({ isServerLayer: true })]
: // When using Babel, we will have to add the SWC loader
// as an additional pass to handle RSC correctly.
// This will cause some performance overhead but
// acceptable as Babel will not be recommended.
[getSwcLoader({ isServerLayer: true }), getBabelLoader()]
: []
const swcLoaderForClientLayer = hasServerComponents
? useSWCLoader
? [getSwcLoader({ isServerLayer: false })]
: // When using Babel, we will have to add the SWC loader
// as an additional pass to handle RSC correctly.
// This will cause some performance overhead but
// acceptable as Babel will not be recommended.
[getSwcLoader({ isServerLayer: false }), getBabelLoader()]
: []

// Loader for API routes needs to be differently configured as it shouldn't
// have RSC transpiler enabled, so syntax checks such as invalid imports won't
Expand Down Expand Up @@ -1699,6 +1708,7 @@ export default async function getBaseWebpackConfig(
'next-flight-loader',
'next-flight-client-entry-loader',
'next-flight-action-entry-loader',
'next-flight-client-action-loader',
'noop-loader',
'next-middleware-loader',
'next-edge-function-loader',
Expand Down Expand Up @@ -1727,7 +1737,9 @@ export default async function getBaseWebpackConfig(
...(hasAppDir && !isClient
? [
{
issuerLayer: WEBPACK_LAYERS.server,
issuerLayer: {
or: [WEBPACK_LAYERS.server, WEBPACK_LAYERS.action],
},
test: {
// Resolve it if it is a source code file, and it has NOT been
// opted out of bundling.
Expand Down Expand Up @@ -1796,6 +1808,7 @@ export default async function getBaseWebpackConfig(
WEBPACK_LAYERS.server,
WEBPACK_LAYERS.client,
WEBPACK_LAYERS.appClient,
WEBPACK_LAYERS.action,
],
},
resolve: {
Expand All @@ -1817,7 +1830,9 @@ export default async function getBaseWebpackConfig(
oneOf: [
{
exclude: [staticGenerationAsyncStorageRegex],
issuerLayer: WEBPACK_LAYERS.server,
issuerLayer: {
or: [WEBPACK_LAYERS.server, WEBPACK_LAYERS.action],
},
test: {
// Resolve it if it is a source code file, and it has NOT been
// opted out of bundling.
Expand Down Expand Up @@ -1888,7 +1903,9 @@ export default async function getBaseWebpackConfig(
? [
{
test: codeCondition.test,
issuerLayer: WEBPACK_LAYERS.server,
issuerLayer: {
or: [WEBPACK_LAYERS.server, WEBPACK_LAYERS.action],
},
exclude: [staticGenerationAsyncStorageRegex],
use: swcLoaderForServerLayer,
},
Expand All @@ -1897,6 +1914,27 @@ export default async function getBaseWebpackConfig(
resourceQuery: /__edge_ssr_entry__/,
use: swcLoaderForServerLayer,
},
{
test: codeCondition.test,
issuerLayer: {
or: [WEBPACK_LAYERS.client, WEBPACK_LAYERS.appClient],
},
exclude: [staticGenerationAsyncStorageRegex],
use: [
...(dev && isClient
? [
require.resolve(
'next/dist/compiled/@next/react-refresh-utils/dist/loader'
),
defaultLoaders.babel,
]
: []),
{
loader: 'next-flight-client-action-loader',
},
...swcLoaderForClientLayer,
],
},
]
: []),
{
Expand Down Expand Up @@ -2037,7 +2075,9 @@ export default async function getBaseWebpackConfig(
{
test: /node_modules[/\\]client-only[/\\]error.js/,
loader: 'next-invalid-import-error-loader',
issuerLayer: WEBPACK_LAYERS.server,
issuerLayer: {
or: [WEBPACK_LAYERS.server, WEBPACK_LAYERS.action],
},
options: {
message:
"'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getRSCModuleInformation } from './../../analysis/get-page-static-info'
import { getModuleBuildInfo } from './get-module-build-info'

export default async function transformSource(
this: any,
source: string,
sourceMap: any
) {
// Avoid buffer to be consumed
if (typeof source !== 'string') {
throw new Error('Expected source to have been transformed to a string.')
}

const callback = this.async()

// Assign the RSC meta information to buildInfo.
const buildInfo = getModuleBuildInfo(this._module)
buildInfo.rsc = getRSCModuleInformation(source, false)

// This is a server action entry module in the client layer. We need to attach
// noop exports of `callServer` wrappers for each action.
if (buildInfo.rsc?.actions) {
source = `
import { callServer } from 'next/dist/client/app-call-server'

function __build_action__(action, args) {
return callServer(action.$$id, args)
}

${source}
`
}

return callback(null, source, sourceMap)
}
Loading