diff --git a/package.json b/package.json index e92f23858..16506f1ad 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,13 @@ "dependencies": { "@swc/core": "1.3.39", "@vitejs/plugin-react": "^3.1.0", + "busboy": "^1.6.0", "vite": "^4.1.4" }, "devDependencies": { "@swc/cli": "^0.1.62", "@types/babel__core": "^7.20.0", + "@types/busboy": "^1.5.0", "@types/node": "^18.15.0", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90a5ad37a..de623cd23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,10 +4,12 @@ specifiers: '@swc/cli': ^0.1.62 '@swc/core': 1.3.39 '@types/babel__core': ^7.20.0 + '@types/busboy': ^1.5.0 '@types/node': ^18.15.0 '@types/react': ^18.0.28 '@types/react-dom': ^18.0.11 '@vitejs/plugin-react': ^3.1.0 + busboy: ^1.6.0 nodemon: ^2.0.21 react: 0.0.0-experimental-f828bad38-20230313 react-dom: 0.0.0-experimental-f828bad38-20230313 @@ -18,11 +20,13 @@ specifiers: dependencies: '@swc/core': 1.3.39 '@vitejs/plugin-react': 3.1.0_vite@4.1.4 + busboy: 1.6.0 vite: 4.1.4_@types+node@18.15.0 devDependencies: '@swc/cli': 0.1.62_@swc+core@1.3.39 '@types/babel__core': 7.20.0 + '@types/busboy': 1.5.0 '@types/node': 18.15.0 '@types/react': 18.0.28 '@types/react-dom': 18.0.11 @@ -690,6 +694,12 @@ packages: '@babel/types': 7.21.2 dev: true + /@types/busboy/1.5.0: + resolution: {integrity: sha512-ncOOhwmyFDW76c/Tuvv9MA9VGYUCn8blzyWmzYELcNGDb0WXWLSmFi7hJq25YdRBYJrmMBB5jZZwUjlJe9HCjQ==} + dependencies: + '@types/node': 18.15.0 + dev: true + /@types/cacheable-request/6.0.3: resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} dependencies: @@ -844,6 +854,13 @@ packages: update-browserslist-db: 1.0.10_browserslist@4.21.5 dev: false + /busboy/1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + /cacheable-lookup/5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} engines: {node: '>=10.6.0'} @@ -1767,6 +1784,11 @@ packages: engines: {node: '>= 8'} dev: true + /streamsearch/1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + /string_decoder/1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: diff --git a/src/client.ts b/src/client.ts index 4bac492d2..83664fd7a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,7 +1,7 @@ import type { ReactNode } from "react"; import RSDWClient from "react-server-dom-webpack/client"; -const { createFromFetch } = RSDWClient; +const { createFromFetch, encodeReply } = RSDWClient; export function serve(rscId: string, render: (ele: ReactNode) => void) { return async (props: Props) => { @@ -10,7 +10,8 @@ export function serve(rscId: string, render: (ele: ReactNode) => void) { searchParams.set("rsc_id", rscId); searchParams.set("props", serializedProps); const options = { - callServer(rsfId: string, args: unknown[]) { + async callServer(rsfId: string, args: unknown[]) { + const isMutating = !!mutationMode; const searchParams = new URLSearchParams(); searchParams.set("rsf_id", rsfId); if (isMutating) { @@ -19,7 +20,7 @@ export function serve(rscId: string, render: (ele: ReactNode) => void) { } const response = fetch(`/?${searchParams}`, { method: "POST", - body: JSON.stringify(args), + body: await encodeReply(args), }); const data = createFromFetch(response, options); if (isMutating) { @@ -38,13 +39,13 @@ export function serve(rscId: string, render: (ele: ReactNode) => void) { }; } -let isMutating = 0; +let mutationMode = 0; export function mutate any>(fn: Fn): Fn { return ((...args: unknown[]) => { - ++isMutating; + ++mutationMode; const result = fn(...args); - --isMutating; + --mutationMode; return result; }) as Fn; } diff --git a/src/middleware/rscDev.ts b/src/middleware/rscDev.ts index f72638c9f..f81fd3faa 100644 --- a/src/middleware/rscDev.ts +++ b/src/middleware/rscDev.ts @@ -5,10 +5,12 @@ import url from "node:url"; import * as swc from "@swc/core"; import RSDWRegister from "react-server-dom-webpack/node-register"; import RSDWServer from "react-server-dom-webpack/server"; +import busboy from "busboy"; import type { MiddlewareCreator } from "./common.ts"; -const { renderToPipeableStream } = RSDWServer; +const { renderToPipeableStream, decodeReply, decodeReplyFromBusboy } = + RSDWServer; // TODO we would like a native solution without hacks // https://nodejs.org/api/esm.html#loaders @@ -80,11 +82,21 @@ const rscDefault: MiddlewareCreator = (config) => { if (typeof rsfId === "string") { const [filePath, name] = rsfId.split("#"); const fname = path.join(dir, filePath!); - let body = ""; - for await (const chunk of req) { - body += chunk; + let args: unknown[] = []; + if (req.headers["content-type"]?.startsWith("multipart/form-data")) { + const bb = busboy({ headers: req.headers }); + const reply = decodeReplyFromBusboy(bb); + req.pipe(bb); + args = await reply; + } else { + let body = ""; + for await (const chunk of req) { + body += chunk; + } + if (body) { + args = await decodeReply(body); + } } - const args = body ? JSON.parse(body) : []; // TODO can we use node:vm? const mod = require(fname); const data = await (mod[name!] || mod)(...args);