Skip to content

Commit 61c9a4a

Browse files
authored
fix: RSC fails to properly handle Windows paths (#12969)
1 parent 17557bf commit 61c9a4a

File tree

9 files changed

+216
-44
lines changed

9 files changed

+216
-44
lines changed

crates/rspack_loader_swc/src/rsc_transforms/to_module_ref.rs

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use rspack_core::{Module, NormalModule, RscModuleType};
33
use rspack_error::{Result, ToStringResultToRspackResultExt};
44
use swc::atoms::Wtf8Atom;
55

6-
fn to_cjs_server_entry(resource: &str, server_refs: &[Wtf8Atom]) -> String {
6+
fn to_cjs_server_entry(resource: &str, server_refs: &[Wtf8Atom]) -> Result<String> {
77
let mut cjs_source =
88
"const { createServerEntry } = require(\"react-server-dom-rspack/server\");\n".to_string();
99

@@ -12,35 +12,38 @@ fn to_cjs_server_entry(resource: &str, server_refs: &[Wtf8Atom]) -> String {
1212
Some("default") => {
1313
cjs_source.push_str(&formatdoc! {
1414
r#"
15-
const _default = require("{resource}?rsc-server-entry-proxy=true");
15+
const _default = require({request});
1616
module.exports = createServerEntry(
1717
_default,
18-
"{resource}",
18+
{resource}
1919
);
2020
"#,
21-
resource = resource
21+
request = serde_json::to_string(&format!("{resource}?rsc-server-entry-proxy=true")).to_rspack_result()?,
22+
resource = serde_json::to_string(&resource).to_rspack_result()?
2223
});
2324
}
2425
Some(ident) => {
2526
cjs_source.push_str(&formatdoc! {
2627
r#"
27-
const _original_{ident} = require("{resource}?rsc-server-entry-proxy=true").{ident};
28+
const _original_{ident} = require({request}).{ident};
2829
exports.{ident} = createServerEntry(
2930
_original_{ident},
30-
"{resource}",
31+
{resource}
3132
);
3233
"#,
3334
ident = ident,
34-
resource = resource
35+
request = serde_json::to_string(&format!("{resource}?rsc-server-entry-proxy=true")).to_rspack_result()?,
36+
resource = serde_json::to_string(&resource).to_rspack_result()?
3537
});
3638
}
3739
_ => {}
3840
}
3941
}
40-
cjs_source
42+
43+
Ok(cjs_source)
4144
}
4245

43-
fn to_esm_server_entry(resource: &str, server_refs: &[Wtf8Atom]) -> String {
46+
fn to_esm_server_entry(resource: &str, server_refs: &[Wtf8Atom]) -> Result<String> {
4447
let mut esm_source =
4548
"import { createServerEntry } from \"react-server-dom-rspack/server\";\n".to_string();
4649

@@ -49,45 +52,50 @@ fn to_esm_server_entry(resource: &str, server_refs: &[Wtf8Atom]) -> String {
4952
Some("default") => {
5053
esm_source.push_str(&formatdoc! {
5154
r#"
52-
import _default from "{resource}?rsc-server-entry-proxy=true";
55+
import _default from {request};
5356
export default createServerEntry(
5457
_default,
55-
"{resource}",
56-
)
58+
{resource}
59+
);
5760
"#,
58-
resource = resource
61+
request = serde_json::to_string(&format!("{resource}?rsc-server-entry-proxy=true")).to_rspack_result()?,
62+
resource = serde_json::to_string(&resource).to_rspack_result()?
5963
});
6064
}
6165
Some(ident) => {
6266
esm_source.push_str(&formatdoc! {
6367
r#"
64-
import {{ {ident} as _original_{ident} }} from "{resource}?rsc-server-entry-proxy=true";
68+
import {{ {ident} as _original_{ident} }} from {request};
6569
export const {ident} = createServerEntry(
6670
_original_{ident},
67-
"{resource}",
68-
)
71+
{resource}
72+
);
6973
"#,
7074
ident = ident,
71-
resource = resource,
75+
request = serde_json::to_string(&format!("{resource}?rsc-server-entry-proxy=true")).to_rspack_result()?,
76+
resource = serde_json::to_string(&resource).to_rspack_result()?
7277
});
7378
}
7479
_ => {}
7580
}
7681
}
77-
esm_source
82+
83+
Ok(esm_source)
7884
}
7985

8086
fn to_esm_client_entry(resource: &str, client_refs: &[Wtf8Atom]) -> Result<String> {
8187
let mut esm_source =
8288
String::from("import { registerClientReference } from \"react-server-dom-rspack/server\"\n");
8389

84-
let call_error = format!(
85-
"Attempted to call the default export of {} from \
90+
let resource_literal = serde_json::to_string(resource).to_rspack_result()?;
91+
92+
let call_error_literal = serde_json::to_string(&format!(
93+
"Attempted to call the default export of {resource_literal} from \
8694
the server, but it's on the client. It's not possible to invoke a \
8795
client function from the server, it can only be rendered as a \
88-
Component or passed to props of a Client Component.",
89-
serde_json::to_string(resource).to_rspack_result()?
90-
);
96+
Component or passed to props of a Client Component."
97+
))
98+
.to_rspack_result()?;
9199

92100
for client_ref in client_refs {
93101
match client_ref.as_str() {
@@ -96,31 +104,32 @@ fn to_esm_client_entry(resource: &str, client_refs: &[Wtf8Atom]) -> Result<Strin
96104
r#"
97105
export default registerClientReference(
98106
function() {{ throw new Error({call_error}) }},
99-
"{resource}",
100-
"default",
107+
{resource},
108+
"default"
101109
)
102110
"#,
103-
resource = resource,
104-
call_error = serde_json::to_string(&call_error).to_rspack_result()?
111+
resource = resource_literal,
112+
call_error = call_error_literal
105113
});
106114
}
107115
Some(ident) => {
108116
esm_source.push_str(&formatdoc! {
109117
r#"
110118
export const {ident} = registerClientReference(
111119
function() {{ throw new Error({call_error}) }},
112-
"{resource}",
120+
{resource},
113121
"{ident}",
114122
)
115123
"#,
116124
ident = ident,
117-
resource = resource,
118-
call_error = serde_json::to_string(&call_error).to_rspack_result()?
125+
resource = resource_literal,
126+
call_error = call_error_literal
119127
});
120128
}
121129
_ => {}
122130
}
123131
}
132+
124133
Ok(esm_source)
125134
}
126135

@@ -129,13 +138,15 @@ fn to_cjs_client_entry(resource: &str, client_refs: &[Wtf8Atom]) -> Result<Strin
129138
"const { registerClientReference } = require(\"react-server-dom-rspack/server\");\n",
130139
);
131140

132-
let call_error = format!(
133-
"Attempted to call the default export of {} from \
141+
let resource_literal = serde_json::to_string(resource).to_rspack_result()?;
142+
143+
let call_error_literal = serde_json::to_string(&format!(
144+
"Attempted to call the default export of {resource_literal} from \
134145
the server, but it's on the client. It's not possible to invoke a \
135146
client function from the server, it can only be rendered as a \
136-
Component or passed to props of a Client Component.",
137-
serde_json::to_string(resource).to_rspack_result()?
138-
);
147+
Component or passed to props of a Client Component."
148+
))
149+
.to_rspack_result()?;
139150

140151
for client_ref in client_refs {
141152
match client_ref.as_str() {
@@ -144,26 +155,26 @@ fn to_cjs_client_entry(resource: &str, client_refs: &[Wtf8Atom]) -> Result<Strin
144155
r#"
145156
module.exports = registerClientReference(
146157
function() {{ throw new Error({call_error}) }},
147-
"{resource}",
158+
{resource},
148159
"default",
149160
);
150161
"#,
151-
resource = resource,
152-
call_error = serde_json::to_string(&call_error).to_rspack_result()?
162+
resource = resource_literal,
163+
call_error = call_error_literal
153164
});
154165
}
155166
Some(ident) => {
156167
cjs_source.push_str(&formatdoc! {
157168
r#"
158169
exports.{ident} = registerClientReference(
159170
function() {{ throw new Error({call_error}) }},
160-
"{resource}",
171+
{resource},
161172
"{ident}",
162173
);
163174
"#,
164175
ident = ident,
165-
resource = resource,
166-
call_error = serde_json::to_string(&call_error).to_rspack_result()?
176+
resource = resource_literal,
177+
call_error = call_error_literal
167178
});
168179
}
169180
_ => {}
@@ -196,9 +207,9 @@ pub fn to_module_ref(module: &NormalModule) -> Result<Option<String>> {
196207
));
197208
}
198209
if rsc.is_cjs {
199-
return Ok(Some(to_cjs_server_entry(resource, &rsc.server_refs)));
210+
return Ok(Some(to_cjs_server_entry(resource, &rsc.server_refs)?));
200211
} else {
201-
return Ok(Some(to_esm_server_entry(resource, &rsc.server_refs)));
212+
return Ok(Some(to_esm_server_entry(resource, &rsc.server_refs)?));
202213
}
203214
}
204215

crates/rspack_plugin_rsc/src/server_plugin.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,9 @@ impl RscServerPlugin {
207207
for (dep, actions) in component_info.action_imports {
208208
action_entry_imports.insert(dep, actions);
209209
}
210-
if !component_info.client_component_imports.is_empty() {
210+
if !component_info.client_component_imports.is_empty()
211+
|| !component_info.css_imports.is_empty()
212+
{
211213
client_entries_to_inject.push(ClientEntry {
212214
entry_name: entry_name.clone(),
213215
runtime: runtime.clone(),
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
const cssRule = {
36+
test: /\.css$/,
37+
type: 'css/auto',
38+
};
39+
40+
module.exports = [
41+
{
42+
mode: 'production',
43+
target: 'node',
44+
entry: {
45+
main: {
46+
import: ssrEntry,
47+
},
48+
},
49+
resolve: {
50+
extensions: ['...', '.ts', '.tsx', '.jsx'],
51+
},
52+
module: {
53+
rules: [
54+
cssRule,
55+
swcLoaderRule,
56+
{
57+
resource: ssrEntry,
58+
layer: Layers.ssr,
59+
},
60+
{
61+
resource: rscEntry,
62+
layer: Layers.rsc,
63+
resolve: {
64+
conditionNames: ['react-server', '...'],
65+
},
66+
},
67+
{
68+
issuerLayer: Layers.rsc,
69+
resolve: {
70+
conditionNames: ['react-server', '...'],
71+
},
72+
},
73+
],
74+
},
75+
plugins: [
76+
new ServerPlugin(),
77+
new rspack.DefinePlugin({
78+
CLIENT_PATH: JSON.stringify(path.resolve(__dirname, 'src/Client.js')),
79+
}),
80+
],
81+
optimization: {
82+
moduleIds: 'named',
83+
concatenateModules: true,
84+
},
85+
// TODO: enable lazy compilation when it works with RSC
86+
lazyCompilation: false,
87+
},
88+
{
89+
mode: 'production',
90+
target: 'web',
91+
entry: {
92+
main: {
93+
import: './src/framework/entry.client.js',
94+
},
95+
},
96+
resolve: {
97+
extensions: ['...', '.ts', '.tsx', '.jsx'],
98+
},
99+
module: {
100+
rules: [
101+
cssRule,
102+
swcLoaderRule
103+
],
104+
},
105+
plugins: [new ClientPlugin()],
106+
optimization: {
107+
moduleIds: 'named',
108+
concatenateModules: true,
109+
},
110+
// TODO: enable lazy compilation when it works with RSC
111+
lazyCompilation: false,
112+
},
113+
];
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
body {
2+
background-color: purple;
3+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"use server-entry";
2+
3+
import "./App.css";
4+
5+
export const App = async () => {
6+
return (
7+
<h1>RSC App</h1>
8+
);
9+
};
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { renderToReadableStream } from 'react-server-dom-rspack/server';
2+
import { App } from '../App';
3+
4+
export const renderRscStream = () => {
5+
return renderToReadableStream(<App />);
6+
};
7+
8+
it('should expose entry JS and CSS files for server entries', async () => {
9+
expect(App.entryJsFiles).toBeDefined();
10+
expect(App.entryCssFiles).toBeDefined();
11+
12+
expect(App.entryJsFiles.length).toEqual(1);
13+
expect(App.entryJsFiles[0]).toMatch(/\.js$/);
14+
15+
expect(App.entryCssFiles.length).toEqual(1);
16+
expect(App.entryCssFiles[0]).toMatch(/\.css$/);
17+
});
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)