Skip to content

Commit 161b4a5

Browse files
authored
fix(react-server-dom-vite): fix action imported by client (#80)
1 parent e21f222 commit 161b4a5

File tree

7 files changed

+103
-8
lines changed

7 files changed

+103
-8
lines changed

react-server-dom-vite-example/e2e/basic.test.ts

+38-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Page, test } from "@playwright/test";
1+
import { type Page, expect, test } from "@playwright/test";
22
import { createEditor } from "./helper";
33

44
test("client reference", async ({ page }) => {
@@ -14,13 +14,13 @@ test("client reference", async ({ page }) => {
1414
await page.getByText("Client counter: 0").click();
1515
});
1616

17-
test("server reference @js", async ({ page }) => {
17+
test("server reference in server @js", async ({ page }) => {
1818
await testServerAction(page);
1919
});
2020

2121
test.describe(() => {
2222
test.use({ javaScriptEnabled: false });
23-
test("server reference @nojs", async ({ page }) => {
23+
test("server reference in server @nojs", async ({ page }) => {
2424
await testServerAction(page);
2525
});
2626
});
@@ -42,6 +42,41 @@ async function testServerAction(page: Page) {
4242
await page.getByText("Server counter: 0").click();
4343
}
4444

45+
test("server reference in client @js", async ({ page }) => {
46+
await testServerAction2(page, { js: true });
47+
});
48+
49+
test.describe(() => {
50+
test.use({ javaScriptEnabled: false });
51+
test("server reference in client @nojs", async ({ page }) => {
52+
await testServerAction2(page, { js: false });
53+
});
54+
});
55+
56+
async function testServerAction2(page: Page, options: { js: boolean }) {
57+
await page.goto("/");
58+
if (options.js) {
59+
await page.getByText("[hydrated: 1]").click();
60+
}
61+
await page.locator('input[name="x"]').fill("2");
62+
await page.locator('input[name="y"]').fill("3");
63+
await page.locator('input[name="y"]').press("Enter");
64+
await expect(page.getByTestId("calculator-answer")).toContainText("5");
65+
await page.locator('input[name="x"]').fill("2");
66+
await page.locator('input[name="y"]').fill("three");
67+
await page.locator('input[name="y"]').press("Enter");
68+
await expect(page.getByTestId("calculator-answer")).toContainText(
69+
"(invalid input)",
70+
);
71+
if (options.js) {
72+
await expect(page.locator('input[name="x"]')).toHaveValue("2");
73+
await expect(page.locator('input[name="y"]')).toHaveValue("three");
74+
} else {
75+
await expect(page.locator('input[name="x"]')).toHaveValue("");
76+
await expect(page.locator('input[name="y"]')).toHaveValue("");
77+
}
78+
}
79+
4580
test("client hmr @dev", async ({ page }) => {
4681
await page.goto("/");
4782
await page.getByText("[hydrated: 1]").click();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"use server";
2+
3+
export async function add(_prev: unknown, formData: FormData) {
4+
let x = formData.get("x");
5+
let y = formData.get("y");
6+
if (typeof x === "string" && typeof y === "string") {
7+
let x2 = parseFloat(x);
8+
let y2 = parseFloat(y);
9+
if (!Number.isNaN(x2) && !Number.isNaN(y2)) {
10+
return x2 + y2;
11+
}
12+
}
13+
return "(invalid input)";
14+
}

react-server-dom-vite-example/src/app/client.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import React from "react";
4+
import { add } from "./action-by-client";
45

56
export function Counter() {
67
const [count, setCount] = React.useState(0);
@@ -26,3 +27,36 @@ function useHydrated() {
2627
() => false,
2728
);
2829
}
30+
31+
export function Calculator() {
32+
const [returnValue, formAction, _isPending] = React.useActionState(add, null);
33+
const [x, setX] = React.useState("");
34+
const [y, setY] = React.useState("");
35+
36+
return (
37+
<form
38+
action={formAction}
39+
style={{ padding: "0.5rem" }}
40+
data-testid="calculator"
41+
>
42+
<div>Calculator</div>
43+
<div style={{ display: "flex", gap: "0.3rem" }}>
44+
<input
45+
name="x"
46+
style={{ width: "2rem" }}
47+
value={x}
48+
onChange={(e) => setX(e.target.value)}
49+
/>
50+
+
51+
<input
52+
name="y"
53+
style={{ width: "2rem" }}
54+
value={y}
55+
onChange={(e) => setY(e.target.value)}
56+
/>
57+
=<span data-testid="calculator-answer">{returnValue ?? "?"}</span>
58+
</div>
59+
<button hidden></button>
60+
</form>
61+
);
62+
}

react-server-dom-vite-example/src/app/index.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { changeCounter, getCounter } from "./action";
2-
import { Counter, Hydrated } from "./client";
2+
import { Calculator, Counter, Hydrated } from "./client";
33

44
export async function IndexPage() {
55
return (
@@ -22,6 +22,7 @@ export async function IndexPage() {
2222
</button>
2323
</div>
2424
</form>
25+
<Calculator />
2526
</div>
2627
);
2728
}

react-server-dom-vite-example/src/entry.client.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ async function main() {
2121
setPayload(payload);
2222
return payload.returnValue;
2323
};
24+
Object.assign(globalThis, { __callServer: callServer });
2425

2526
async function onNavigation() {
2627
const url = new URL(window.location.href);

react-server-dom-vite-example/src/entry.ssr.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export default async function handler(
4343
const htmlStream = fromPipeableToWebReadable(
4444
ReactDomServer.renderToPipeableStream(payload.root, {
4545
bootstrapModules: ssrAssets.bootstrapModules,
46+
// @ts-ignore no type?
47+
formState: payload.formState,
4648
}),
4749
);
4850

react-server-dom-vite-example/vite.config.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import {
99
defineConfig,
1010
} from "vite";
1111

12+
// state for build orchestration
1213
let browserManifest: Manifest;
13-
let clientReferences: Record<string, string> = {};
14+
let clientReferences: Record<string, string> = {}; // TODO: normalize id
1415
let serverReferences: Record<string, string> = {};
1516
let buildScan = false;
1617

@@ -166,7 +167,6 @@ export default defineConfig({
166167
],
167168
builder: {
168169
sharedPlugins: true,
169-
sharedConfigBuild: true,
170170
async buildApp(builder) {
171171
buildScan = true;
172172
await builder.build(builder.environments.rsc);
@@ -220,8 +220,8 @@ function vitePluginUseServer(): Plugin[] {
220220
name: vitePluginUseServer.name,
221221
transform(code, id) {
222222
if (/^(("use server")|('use server'))/.test(code)) {
223+
serverReferences[id] = id;
223224
if (this.environment.name === "rsc") {
224-
serverReferences[id] = id;
225225
const matches = code.matchAll(/export async function (\w+)\(/g);
226226
const result = [
227227
code,
@@ -233,7 +233,15 @@ function vitePluginUseServer(): Plugin[] {
233233
].join(";\n");
234234
return { code: result, map: null };
235235
} else {
236-
// TODO
236+
const matches = code.matchAll(/export async function (\w+)\(/g);
237+
const result = [
238+
`import $$ReactClient from "@jacob-ebey/react-server-dom-vite/client"`,
239+
...[...matches].map(
240+
([, name]) =>
241+
`export const ${name} = $$ReactClient.createServerReference(${JSON.stringify(id + "#" + name)}, (...args) => __callServer(...args))`,
242+
),
243+
].join(";\n");
244+
return { code: result, map: null };
237245
}
238246
}
239247
},

0 commit comments

Comments
 (0)