Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-server): custom react server entry #199

Merged
merged 6 commits into from
Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading