Skip to content

Commit b7bcbce

Browse files
authored
fix: prevent server actions from being removed in production build (#12898)
1 parent 9e49bfb commit b7bcbce

File tree

7 files changed

+166
-0
lines changed

7 files changed

+166
-0
lines changed

crates/rspack_plugin_rsc/src/server_plugin.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,11 @@ impl RscServerPlugin {
272272
let included_dependencies: Vec<(DependencyId, RuntimeSpec)> = add_ssr_modules_list
273273
.iter()
274274
.map(|injected| (*injected.add_entry.0.id(), injected.runtime.clone()))
275+
.chain(
276+
add_action_entry_list
277+
.iter()
278+
.map(|injected| (*injected.add_entry.0.id(), injected.runtime.clone())),
279+
)
275280
.collect();
276281
let add_include_args: Vec<(BoxDependency, EntryOptions)> = add_ssr_modules_list
277282
.into_iter()
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
const path = require('node:path');
2+
const { rspack, experiments } = require('@rspack/core');
3+
4+
const { createPlugins, Layers } = experiments.rsc;
5+
const { ServerPlugin, ClientPlugin } = createPlugins();
6+
7+
const ssrEntry = path.join(__dirname, 'src/framework/entry.ssr.js');
8+
const rscEntry = path.join(__dirname, 'src/framework/entry.rsc.js');
9+
10+
const swcLoaderRule = {
11+
test: /\.jsx?$/,
12+
use: [
13+
{
14+
loader: 'builtin:swc-loader',
15+
options: {
16+
jsc: {
17+
parser: {
18+
syntax: 'ecmascript',
19+
jsx: true,
20+
},
21+
transform: {
22+
react: {
23+
runtime: 'automatic',
24+
},
25+
},
26+
},
27+
rspackExperiments: {
28+
reactServerComponents: true,
29+
},
30+
},
31+
},
32+
],
33+
};
34+
35+
module.exports = [
36+
{
37+
mode: 'production',
38+
target: 'node',
39+
entry: {
40+
main: {
41+
import: ssrEntry,
42+
},
43+
},
44+
resolve: {
45+
extensions: ['...', '.ts', '.tsx', '.jsx'],
46+
},
47+
module: {
48+
rules: [
49+
swcLoaderRule,
50+
{
51+
resource: ssrEntry,
52+
layer: Layers.ssr,
53+
},
54+
{
55+
resource: rscEntry,
56+
layer: Layers.rsc,
57+
resolve: {
58+
conditionNames: ['react-server', '...'],
59+
},
60+
},
61+
{
62+
issuerLayer: Layers.rsc,
63+
resolve: {
64+
conditionNames: ['react-server', '...'],
65+
},
66+
},
67+
],
68+
},
69+
plugins: [
70+
new ServerPlugin(),
71+
new rspack.DefinePlugin({
72+
CLIENT_PATH: JSON.stringify(path.resolve(__dirname, 'src/Client.js')),
73+
}),
74+
],
75+
optimization: {
76+
moduleIds: 'named',
77+
concatenateModules: true,
78+
},
79+
// TODO: enable lazy compilation when it works with RSC
80+
lazyCompilation: false,
81+
},
82+
{
83+
mode: 'production',
84+
target: 'web',
85+
entry: {
86+
main: {
87+
import: './src/framework/entry.client.js',
88+
},
89+
},
90+
resolve: {
91+
extensions: ['...', '.ts', '.tsx', '.jsx'],
92+
},
93+
module: {
94+
rules: [swcLoaderRule],
95+
},
96+
plugins: [new ClientPlugin()],
97+
optimization: {
98+
moduleIds: 'named',
99+
concatenateModules: true,
100+
},
101+
// TODO: enable lazy compilation when it works with RSC
102+
lazyCompilation: false,
103+
},
104+
];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { add, del, get, update } from './actions';
2+
3+
export const App = async () => {
4+
await add();
5+
await del();
6+
await get();
7+
await update();
8+
9+
return (
10+
<h1>RSC App</h1>
11+
);
12+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use server';
2+
3+
export async function add() { }
4+
5+
export async function del() { }
6+
7+
export async function get() { }
8+
9+
export async function update() { }
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// In a real app this entry would consume the RSC payload and hydrate.
2+
// This file exists mainly to mirror the typical split of RSC/SSR/client entries.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {
2+
loadServerAction,
3+
renderToReadableStream,
4+
} from 'react-server-dom-rspack/server';
5+
import { App } from '../App';
6+
7+
export const renderRscStream = () => {
8+
return renderToReadableStream(<App />);
9+
};
10+
11+
it('should preserve all server actions in production build', async () => {
12+
const manifest = __rspack_rsc_manifest__;
13+
expect(manifest).toBeDefined();
14+
15+
const { serverManifest } = manifest;
16+
expect(serverManifest).toBeDefined();
17+
18+
const actionIds = Object.keys(serverManifest);
19+
expect(actionIds).toHaveLength(4);
20+
21+
// Ensure all collected actions are loadable server actions.
22+
actionIds.forEach((actionId) => {
23+
expect(loadServerAction(actionId)).toEqual(expect.any(Function));
24+
});
25+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createFromReadableStream } from 'react-server-dom-rspack/client';
2+
import { renderRscStream } from './entry.rsc';
3+
4+
export const renderHTML = async () => {
5+
// In real SSR, the HTML renderer would consume the RSC stream.
6+
// For this test case we just ensure the pipeline can be invoked.
7+
const rscStream = await renderRscStream();
8+
return createFromReadableStream(rscStream);
9+
};

0 commit comments

Comments
 (0)