-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(react-server): server action (#18)
* wip: re-exports react server * wip: serverActionHandler * wip: plugin * chore: comment * wip * wip * wip: parseExports * refactor: parseExports * wip: transform plugin * wip: virtual * wip: call server * chore: readme * build: fix server entry * test: update e2e
- Loading branch information
Showing
15 changed files
with
387 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,38 @@ | ||
import { test, expect } from "@playwright/test"; | ||
import { test, type Page } from "@playwright/test"; | ||
|
||
test("basic", async ({ page }) => { | ||
test("client-component", async ({ page }) => { | ||
await page.goto("/"); | ||
await expect(page.locator("#root")).toContainText("hydrated: true"); | ||
await expect(page.locator("#root")).toContainText("Count: 0"); | ||
await page.getByRole("button", { name: "+" }).click(); | ||
await expect(page.locator("#root")).toContainText("Count: 1"); | ||
await page.getByText("hydrated: true").click(); | ||
await page.getByTestId("client-component").getByText("Count: 0").click(); | ||
await page | ||
.getByTestId("client-component") | ||
.getByRole("button", { name: "+" }) | ||
.click(); | ||
await page.getByTestId("client-component").getByText("Count: 1").click(); | ||
}); | ||
|
||
test("server-action @js", async ({ page }) => { | ||
await page.goto("/"); | ||
await page.getByText("hydrated: true").click(); | ||
await testServerAction(page); | ||
}); | ||
|
||
test("server-action @nojs", async ({ browser }) => { | ||
const page = await browser.newPage({ javaScriptEnabled: false }); | ||
await page.goto("/"); | ||
await testServerAction(page); | ||
}); | ||
|
||
async function testServerAction(page: Page) { | ||
await page.getByTestId("server-action").getByText("Count: 0").click(); | ||
await page | ||
.getByTestId("server-action") | ||
.getByRole("button", { name: "+" }) | ||
.click(); | ||
await page.getByTestId("server-action").getByText("Count: 1").click(); | ||
await page | ||
.getByTestId("server-action") | ||
.getByRole("button", { name: "-" }) | ||
.click(); | ||
await page.getByTestId("server-action").getByText("Count: 0").click(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import reactServerDomClient from "react-server-dom-webpack/client.browser"; | ||
import { __global } from "../../global"; | ||
|
||
export function createServerReference(id: string) { | ||
return reactServerDomClient.createServerReference(id, (...args) => | ||
__global.callServer(...args), | ||
); | ||
} |
35 changes: 35 additions & 0 deletions
35
examples/react-server/src/features/server-action/react-server.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import reactServerDomWebpack from "react-server-dom-webpack/server.edge"; | ||
import { tinyassert } from "@hiogawa/utils"; | ||
import { ejectActionId } from "./utils"; | ||
|
||
export function registerServerReference( | ||
action: Function, | ||
id: string, | ||
name: string, | ||
) { | ||
return reactServerDomWebpack.registerServerReference(action, id, name); | ||
} | ||
|
||
export async function serverActionHandler({ request }: { request: Request }) { | ||
const formData = await request.formData(); | ||
const actionId = ejectActionId(formData); | ||
const action = await importServerAction(actionId); | ||
await action(formData); | ||
} | ||
|
||
async function importServerReference(id: string): Promise<unknown> { | ||
if (import.meta.env.DEV) { | ||
return import(/* @vite-ignore */ id); | ||
} else { | ||
const references = await import("virtual:server-reference" as string); | ||
const dynImport = references.default[id]; | ||
tinyassert(dynImport, `server reference not found '${id}'`); | ||
return dynImport(); | ||
} | ||
} | ||
|
||
async function importServerAction(id: string): Promise<Function> { | ||
const [file, name] = id.split("#") as [string, string]; | ||
const mod: any = await importServerReference(file); | ||
return mod[name]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import reactServerDomClient from "react-server-dom-webpack/client.edge"; | ||
|
||
export function createServerReference(id: string) { | ||
return reactServerDomClient.createServerReference(id, (...args) => { | ||
console.error(args); | ||
throw new Error("unexpected callServer during SSR"); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { tinyassert } from "@hiogawa/utils"; | ||
|
||
// uniformly handle simple use cases of form action | ||
// both for progressive enhancement and for client-side request | ||
// without using encodeReply/decodeReply/decodeAction API | ||
const ACTION_ID_PREFIX = "$ACTION_ID_"; | ||
|
||
export function injectActionId(formData: FormData, id: string) { | ||
formData.set(ACTION_ID_PREFIX + id, ""); | ||
} | ||
|
||
export function ejectActionId(formData: FormData) { | ||
let id: string | undefined; | ||
formData.forEach((_v, k) => { | ||
if (k.startsWith(ACTION_ID_PREFIX)) { | ||
id = k.slice(ACTION_ID_PREFIX.length); | ||
formData.delete(k); | ||
} | ||
}); | ||
tinyassert(id); | ||
return id; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
import type { ViteDevServer } from "vite"; | ||
import type { ModuleRunner } from "vite/module-runner"; | ||
import type { CallServerCallback } from "./types"; | ||
|
||
// quick global hacks... | ||
|
||
export const __global: { | ||
server: ViteDevServer; | ||
reactServerRunner: ModuleRunner; | ||
callServer: CallServerCallback; | ||
} = ((globalThis as any).__VITE_REACT_SERVER_GLOBAL ??= {}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
"use server"; | ||
|
||
let count = 0; | ||
|
||
export function getCounter() { | ||
return count; | ||
} | ||
|
||
export function changeCounter(formData: FormData) { | ||
count += Number(formData.get("value")); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,29 @@ | ||
import { changeCounter, getCounter } from "./_action"; | ||
import { ClientComponent } from "./_client"; | ||
|
||
export default function Page() { | ||
return ( | ||
<div> | ||
<h4>Hello react server</h4> | ||
<h4>Hello Server Component</h4> | ||
<ServerActionDemo /> | ||
<ClientComponent /> | ||
</div> | ||
); | ||
} | ||
|
||
function ServerActionDemo() { | ||
return ( | ||
<div data-testid="server-action"> | ||
<h4>Hello Server Action</h4> | ||
<form action={changeCounter}> | ||
<div>Count: {getCounter()}</div> | ||
<button name="value" value={-1}> | ||
-1 | ||
</button> | ||
<button name="value" value={+1}> | ||
+1 | ||
</button> | ||
</form> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.