Skip to content

Commit

Permalink
feat(react-server): custom react server entry (#199)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Mar 17, 2024
1 parent 99f050b commit 2df1569
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 38 deletions.
5 changes: 5 additions & 0 deletions packages/react-server/examples/basic/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ test("RouteProps.request", async ({ page }) => {
await page.getByText('searchParams = {"hello":""}').click();
});

test("custom entry-react-server", async ({ request }) => {
const res = await request.get("/test/__rpc");
expect(await res.json()).toEqual({ hello: "world" });
});

async function setupCheckClientState(page: Page) {
// setup client state
await page.getByPlaceholder("test-input").fill("hello");
Expand Down
16 changes: 16 additions & 0 deletions packages/react-server/examples/basic/src/entry-react-server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
type ReactServerHandler,
handler as baseHandler,
} from "@hiogawa/react-server/entry-react-server";

export const handler: ReactServerHandler = async (ctx) => {
const url = new URL(ctx.request.url);
if (url.pathname === "/test/__rpc") {
return new Response(JSON.stringify({ hello: "world" }), {
headers: {
"content-type": "application/json",
},
});
}
return baseHandler(ctx);
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export default function Page(props: PageRouteProps) {
</Link>
</div>
</div>
<h5 className="font-bold">custom entry</h5>
<div>
<a className="antd-link" href="/test/__rpc">
/text/__rpc
</a>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions packages/react-server/examples/basic/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default defineConfig({
plugins: [
react(),
vitePluginReactServer({
entry: "/src/entry-react-server.tsx",
plugins: [testVitePluginVirtual()],
}),
vitePluginLogger(),
Expand Down
55 changes: 50 additions & 5 deletions packages/react-server/src/entry/react-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,64 @@ import { objectMapKeys } from "@hiogawa/utils";
import reactServerDomServer from "react-server-dom-webpack/server.edge";
import { generateRouteTree, matchRoute, renderMatchRoute } from "../lib/router";
import { createBundlerConfig } from "../lib/rsc";
import { ejectActionId } from "../lib/shared";
import { ejectActionId, unwrapRscRequest } from "../lib/shared";

export type ReactServerHandler = (
ctx: ReactServerHandlerContext,
) => Promise<ReactServerHandlerResult>;

// users can extend interface
export interface ReactServerHandlerContext {
request: Request;
}

export type ReactServerHandlerResult =
| Response
| {
stream: ReadableStream<Uint8Array>;
status: number;
};

export const handler: ReactServerHandler = async ({ request }) => {
// TODO
// api to manipulate response status/headers from server action/component?
// allow mutate them via PageRouterProps?
// also redirect?

// action
if (request.method === "POST") {
await actionHandler({ request });
}

// check rsc-only request
const rscOnlyRequest = unwrapRscRequest(request);

// rsc
const { stream, status } = render({
request: rscOnlyRequest ?? request,
});
if (rscOnlyRequest) {
return new Response(stream, {
headers: {
"content-type": "text/x-component",
},
});
}

return { stream, status };
};

//
// render RSC
//

export function render({ request }: { request: Request }) {
function render({ request }: { request: Request }) {
const result = router.run(request);
const rscStream = reactServerDomServer.renderToReadableStream(
const stream = reactServerDomServer.renderToReadableStream(
result.node,
createBundlerConfig(),
);
return { rscStream, status: result.match.notFound ? 404 : 200 };
return { stream, status: result.match.notFound ? 404 : 200 };
}

//
Expand Down Expand Up @@ -52,7 +97,7 @@ function createRouter() {
// server action
//

export async function actionHandler({ request }: { request: Request }) {
async function actionHandler({ request }: { request: Request }) {
const formData = await request.formData();
if (0) {
// TODO: proper decoding?
Expand Down
41 changes: 14 additions & 27 deletions packages/react-server/src/entry/server.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,45 @@
import { typedBoolean } from "@hiogawa/utils";
import reactDomServer from "react-dom/server.edge";
import { injectRSCPayload } from "rsc-html-stream/server";
import { unwrapRscRequest } from "../lib/shared";
import {
createModuleMap,
initDomWebpackSsr,
invalidateImportCacheOnFinish,
} from "../lib/ssr";

export async function handler(request: Request): Promise<Response> {
const entryRsc = await importEntryRsc();
const reactServer = await importReactServer();

// action
if (request.method === "POST") {
await entryRsc.actionHandler({ request });
}

// check rsc-only request
const rscRequest = unwrapRscRequest(request);

// rsc
const { rscStream, status } = entryRsc.render({
request: rscRequest ?? request,
});
if (rscRequest) {
return new Response(rscStream, {
headers: {
"content-type": "text/x-component",
},
});
// server action and render rsc
const result = await reactServer.handler({ request });
if (result instanceof Response) {
return result;
}

// ssr rsc
let htmlStream = await renderHtml(rscStream);
const htmlStream = await renderHtml(result.stream);
return new Response(htmlStream, {
status,
status: result.status,
headers: {
"content-type": "text/html",
},
});
}

async function importEntryRsc(): Promise<typeof import("./react-server")> {
export async function importReactServer(): Promise<
typeof import("./react-server")
> {
if (import.meta.env.DEV) {
return __rscDevServer.ssrLoadModule(
"@hiogawa/react-server/entry-react-server",
) as any;
return __rscDevServer.ssrLoadModule(__rscEntry) as any;
} else {
return import("/dist/rsc/index.js" as string);
}
}

// TODO: full <html> render by RSC?
async function renderHtml(rscStream: ReadableStream): Promise<ReadableStream> {
export async function renderHtml(
rscStream: ReadableStream,
): Promise<ReadableStream> {
await initDomWebpackSsr();

const { default: reactServerDomClient } = await import(
Expand Down
1 change: 1 addition & 0 deletions packages/react-server/src/entry/types-global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
// injected by vitePluginReactServer during dev
declare const __devServer: import("vite").ViteDevServer;
declare const __rscDevServer: import("vite").ViteDevServer;
declare const __rscEntry: string;
1 change: 1 addition & 0 deletions packages/react-server/src/lib/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { CallServerCallback } from "./types";

// TODO: organize

// TODO: use accept header x-component?
const RSC_PARAM = "__rsc";

export function wrapRscRequestUrl(url: string): string {
Expand Down
6 changes: 3 additions & 3 deletions packages/react-server/src/lib/types-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ declare module "react-server-dom-webpack/server.edge" {
node: React.ReactNode,
bundlerConfig: import("./types").BundlerConfig,
opitons?: {},
): ReadableStream;
): ReadableStream<Uint8Array>;

export function decodeReply(body: string | FormData): Promise<unknown>;
}

// https://github.com/facebook/react/blob/89021fb4ec9aa82194b0788566e736a4cedfc0e4/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js
declare module "react-server-dom-webpack/client.edge" {
export function createFromReadableStream(
stream: ReadableStream,
stream: ReadableStream<Uint8Array>,
options: {
ssrManifest: import("./types").SsrManifest;
},
Expand All @@ -26,7 +26,7 @@ declare module "react-server-dom-webpack/client.edge" {
// https://github.com/facebook/react/blob/89021fb4ec9aa82194b0788566e736a4cedfc0e4/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js
declare module "react-server-dom-webpack/client.browser" {
export function createFromReadableStream(
stream: ReadableStream,
stream: ReadableStream<Uint8Array>,
options?: {
callServer?: import("./types").CallServerCallback;
},
Expand Down
10 changes: 7 additions & 3 deletions packages/react-server/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@ class RscManager {
}
}

const RSC_ENTRY = "@hiogawa/react-server/entry-react-server";

export function vitePluginReactServer(options?: {
/**
* @default "@hiogawa/react-server/entry-react-server"
*/
entry?: string;
plugins?: PluginOption[];
}): Plugin[] {
const rscEntry = options?.entry ?? "@hiogawa/react-server/entry-react-server";
let parentServer: ViteDevServer | undefined;
let parentEnv: ConfigEnv;
let rscDevServer: ViteDevServer | undefined;
Expand Down Expand Up @@ -141,7 +144,7 @@ export function vitePluginReactServer(options?: {
outDir: "dist/rsc",
rollupOptions: {
input: {
index: RSC_ENTRY,
index: rscEntry,
},
},
},
Expand Down Expand Up @@ -184,6 +187,7 @@ export function vitePluginReactServer(options?: {
Object.assign(globalThis, {
__devServer: parentServer,
__rscDevServer: rscDevServer,
__rscEntry: rscEntry,
});
}
if (parentEnv.command === "build" && !parentEnv.isSsrBuild) {
Expand Down

0 comments on commit 2df1569

Please sign in to comment.