Skip to content

Commit 39d09f7

Browse files
fix(rsc): test client data, apply minor fixes (#13914)
1 parent 17ff101 commit 39d09f7

File tree

8 files changed

+1603
-1507
lines changed

8 files changed

+1603
-1507
lines changed

integration/client-data-test.ts

Lines changed: 1496 additions & 1421 deletions
Large diffs are not rendered by default.

integration/helpers/create-fixture.ts

Lines changed: 98 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import getPort from "get-port";
99
import stripIndent from "strip-indent";
1010
import { sync as spawnSync, spawn } from "cross-spawn";
1111
import type { JsonObject } from "type-fest";
12-
import { createRequestListener } from "@mjackson/node-fetch-server";
1312

1413
import {
1514
type ServerBuild,
@@ -26,6 +25,65 @@ const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
2625
const root = path.join(__dirname, "../..");
2726
const TMP_DIR = path.join(root, ".tmp", "integration");
2827

28+
export async function spawnTestServer({
29+
command,
30+
regex,
31+
validate,
32+
env = {},
33+
cwd,
34+
timeout = 20000,
35+
}: {
36+
command: string[];
37+
regex: RegExp;
38+
validate?: (matches: RegExpMatchArray) => void | Promise<void>;
39+
env?: Record<string, string>;
40+
cwd?: string;
41+
timeout?: number;
42+
}): Promise<{ stop: VoidFunction }> {
43+
return new Promise((accept, reject) => {
44+
let serverProcess = spawn(command[0], command.slice(1), {
45+
env: { ...process.env, ...env },
46+
cwd,
47+
stdio: "pipe",
48+
});
49+
50+
let started = false;
51+
let stdout = "";
52+
let rejectTimeout = setTimeout(() => {
53+
reject(new Error(`Timed out waiting for server to start (${timeout}ms)`));
54+
}, timeout);
55+
56+
serverProcess.stderr.pipe(process.stderr);
57+
serverProcess.stdout.on("data", (chunk: Buffer) => {
58+
if (started) return;
59+
let newChunk = chunk.toString();
60+
stdout += newChunk;
61+
let match = stdout.match(regex);
62+
if (match) {
63+
clearTimeout(rejectTimeout);
64+
started = true;
65+
66+
Promise.resolve(validate?.(match))
67+
.then(() => {
68+
accept({
69+
stop: () => {
70+
serverProcess.kill();
71+
},
72+
});
73+
})
74+
.catch((error: unknown) => {
75+
reject(error);
76+
});
77+
}
78+
});
79+
80+
serverProcess.on("error", (error: unknown) => {
81+
clearTimeout(rejectTimeout);
82+
reject(error);
83+
});
84+
});
85+
}
86+
2987
export interface FixtureInit {
3088
buildStdio?: Writable;
3189
files?: { [filename: string]: string };
@@ -230,66 +288,29 @@ export async function createAppFixture(fixture: Fixture, mode?: ServerMode) {
230288
stop: VoidFunction;
231289
}> => {
232290
if (fixture.useReactRouterServe) {
233-
return new Promise(async (accept, reject) => {
234-
let port = await getPort();
235-
236-
let nodebin = process.argv[0];
237-
let serveProcess = spawn(
238-
nodebin,
239-
[
240-
"node_modules/@react-router/serve/dist/cli.js",
241-
"build/server/index.js",
242-
],
243-
{
244-
env: {
245-
...process.env,
246-
NODE_ENV: mode || "production",
247-
PORT: port.toFixed(0),
248-
},
249-
cwd: fixture.projectDir,
250-
stdio: "pipe",
251-
}
252-
);
253-
// Wait for `started at http://localhost:${port}` to be printed
254-
// and extract the port from it.
255-
let started = false;
256-
let stdout = "";
257-
let rejectTimeout = setTimeout(() => {
258-
reject(
259-
new Error("Timed out waiting for react-router-serve to start")
260-
);
261-
}, 20000);
262-
serveProcess.stderr.pipe(process.stderr);
263-
serveProcess.stdout.on("data", (chunk) => {
264-
if (started) return;
265-
let newChunk = chunk.toString();
266-
stdout += newChunk;
267-
let match: RegExpMatchArray | null = stdout.match(
268-
/\[react-router-serve\] http:\/\/localhost:(\d+)\s/
269-
);
270-
if (match) {
271-
clearTimeout(rejectTimeout);
272-
started = true;
273-
let parsedPort = parseInt(match[1], 10);
274-
275-
if (port !== parsedPort) {
276-
reject(
277-
new Error(
278-
`Expected react-router-serve to start on port ${port}, but it started on port ${parsedPort}`
279-
)
280-
);
281-
return;
282-
}
283-
284-
accept({
285-
stop: () => {
286-
serveProcess.kill();
287-
},
288-
port,
289-
});
291+
let port = await getPort();
292+
let { stop } = await spawnTestServer({
293+
cwd: fixture.projectDir,
294+
command: [
295+
process.argv[0],
296+
"node_modules/@react-router/serve/dist/cli.js",
297+
"build/server/index.js",
298+
],
299+
env: {
300+
NODE_ENV: mode || "production",
301+
PORT: port.toFixed(0),
302+
},
303+
regex: /\[react-router-serve\] http:\/\/localhost:(\d+)\s/,
304+
validate: (matches) => {
305+
let parsedPort = parseInt(matches[1], 10);
306+
if (port !== parsedPort) {
307+
throw new Error(
308+
`Expected react-router-serve to start on port ${port}, but it started on port ${parsedPort}`
309+
);
290310
}
291-
});
311+
},
292312
});
313+
return { stop, port };
293314
}
294315

295316
if (fixture.isSpaMode) {
@@ -334,21 +355,25 @@ export async function createAppFixture(fixture: Fixture, mode?: ServerMode) {
334355
}
335356

336357
if (fixture.templateName.includes("parcel")) {
337-
return new Promise(async (accept) => {
338-
let port = await getPort();
339-
let app = express();
340-
app.use(express.static(path.join(fixture.projectDir, "public")));
341-
app.use(
342-
"/client",
343-
express.static(path.join(fixture.projectDir, "build/client"))
344-
);
345-
346-
app.all("*", createRequestListener(fixture.handler));
347-
348-
let server = app.listen(port);
349-
350-
accept({ stop: server.close.bind(server), port });
358+
let port = await getPort();
359+
let { stop } = await spawnTestServer({
360+
cwd: fixture.projectDir,
361+
command: [process.argv[0], "start.js"],
362+
env: {
363+
NODE_ENV: mode || "production",
364+
PORT: port.toFixed(0),
365+
},
366+
regex: /Server listening on port (\d+)\s/,
367+
validate: (matches) => {
368+
let parsedPort = parseInt(matches[1], 10);
369+
if (port !== parsedPort) {
370+
throw new Error(
371+
`Expected Parcel build server to start on port ${port}, but it started on port ${parsedPort}`
372+
);
373+
}
374+
},
351375
});
376+
return { stop, port };
352377
}
353378

354379
const build = fixture.build;

integration/helpers/rsc-parcel-framework/start.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ app.get("/.well-known/appspecific/com.chrome.devtools.json", (_, res) => {
1515

1616
app.use(createRequestListener(reactRouterRequestHandler));
1717

18-
app.listen(3000);
19-
console.log("Server listening on port 3000 (http://localhost:3000)");
18+
const port = process.env.PORT || 3000;
19+
app.listen(port);
20+
console.log(`Server listening on port ${port} (http://localhost:${port})`);

integration/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
},
1010
"dependencies": {
1111
"@mdx-js/rollup": "^3.1.0",
12-
"@mjackson/node-fetch-server": "0.6.1",
1312
"@playwright/test": "^1.49.1",
1413
"@react-router/dev": "workspace:*",
1514
"@react-router/express": "workspace:*",

packages/react-router/lib/rsc/browser.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ function createRouteFromServerManifest(
664664
...args,
665665
serverAction: async () => {
666666
preventInvalidServerHandlerCall(
667-
"loader",
667+
"action",
668668
match.id,
669669
match.hasLoader
670670
);

packages/react-router/lib/rsc/server.rsc.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ async function getRSCRouteMatch(
852852
),
853853
})
854854
)
855-
: // TODO: Render outet instead?
855+
: // TODO: Render outlet instead?
856856
undefined;
857857
let error: unknown = undefined;
858858

@@ -889,11 +889,6 @@ async function getRSCRouteMatch(
889889
params,
890890
})
891891
)
892-
: match.route.id === "root"
893-
? // FIXME: This should use the `RemixRootDefaultErrorBoundary` but that
894-
// currently uses a hook internally so it fails during RSC. Restructure
895-
// so it can be used safely in an RSC render pass.
896-
React.createElement("p", null, "Loading!")
897892
: undefined;
898893

899894
return {

packages/react-router/lib/rsc/server.ssr.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ export function RSCStaticRouter({
115115

116116
let patchedLoaderData = { ...payload.loaderData };
117117
for (const match of payload.matches) {
118+
// Clear out the loaderData to avoid rendering the route component when the
119+
// route opted into clientLoader hydration and either:
120+
// * gave us a HydrateFallback
121+
// * or doesn't have a server loader and we have no data to render
118122
if (
119123
shouldHydrateRouteLoader(
120124
match.id,

pnpm-lock.yaml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)