diff --git a/packages/vite/package.json b/packages/vite/package.json index 50d605df771b..6a3fc6235534 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -71,6 +71,7 @@ "@vitejs/plugin-react": "4.0.4", "acorn-loose": "8.3.0", "buffer": "6.0.3", + "busboy": "^1.6.0", "core-js": "3.32.2", "dotenv-defaults": "5.0.2", "express": "4.18.2", @@ -83,6 +84,7 @@ }, "devDependencies": { "@babel/cli": "7.22.10", + "@types/busboy": "^1", "@types/express": "4", "@types/react": "18.2.14", "@types/yargs-parser": "21.0.0", diff --git a/packages/vite/src/lib/StatusError.ts b/packages/vite/src/lib/StatusError.ts index 81b1b45e20b9..eae5bb18470a 100644 --- a/packages/vite/src/lib/StatusError.ts +++ b/packages/vite/src/lib/StatusError.ts @@ -3,3 +3,7 @@ export class StatusError extends Error { super(message) } } + +// TODO (RSC): Do we need this? Can we just check instanceof StatusError? +export const hasStatusCode = (x: unknown): x is { statusCode: number } => + typeof (x as any)?.statusCode === 'number' diff --git a/packages/vite/src/runRscFeServer.ts b/packages/vite/src/runRscFeServer.ts index af4cde8bb392..cd6106d05aea 100644 --- a/packages/vite/src/runRscFeServer.ts +++ b/packages/vite/src/runRscFeServer.ts @@ -6,18 +6,23 @@ import fs from 'fs/promises' import path from 'path' +import busboy from 'busboy' // @ts-expect-error We will remove dotenv-defaults from this package anyway import { config as loadDotEnv } from 'dotenv-defaults' import express from 'express' import { createProxyMiddleware } from 'http-proxy-middleware' import isbot from 'isbot' +import RSDWServer from 'react-server-dom-webpack/server.node.unbundled' import type { Manifest as ViteBuildManifest } from 'vite' import { getConfig, getPaths } from '@redwoodjs/project-config' +import { hasStatusCode } from './lib/StatusError' import { registerFwGlobals } from './streaming/registerGlobals' import { renderRSC, setClientEntries } from './waku-lib/rsc-handler-worker' +const { decodeReply, decodeReplyFromBusboy } = RSDWServer + /** * TODO (STREAMING) * We have this server in the vite package only temporarily. @@ -120,62 +125,78 @@ export async function runFeServer() { console.log('basePath', basePath) console.log('req.originalUrl', req.originalUrl, 'req.url', req.url) console.log('req.headers.host', req.headers.host) + const url = new URL(req.originalUrl || '', 'http://' + req.headers.host) let rscId: string | undefined let props = {} let rsfId: string | undefined - const args: unknown[] = [] + let args: unknown[] = [] console.log('url.pathname', url.pathname) + if (url.pathname.startsWith(basePath)) { const index = url.pathname.lastIndexOf('/') + const params = new URLSearchParams(url.pathname.slice(index + 1)) rscId = url.pathname.slice(basePath.length, index) + rsfId = params.get('action_id') || undefined + console.log('rscId', rscId) - const params = new URLSearchParams(url.pathname.slice(index + 1)) + console.log('rsfId', rsfId) + if (rscId && rscId !== '_') { res.setHeader('Content-Type', 'text/x-component') props = JSON.parse(params.get('props') || '{}') } else { rscId = undefined } - rsfId = params.get('action_id') || undefined + if (rsfId) { - console.warn('RSF is not supported yet') - console.warn('RSF is not supported yet') - console.warn('RSF is not supported yet') - // 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); - // } - // } + 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) + } + } } } if (rscId || rsfId) { - const pipeable = await renderRSC({ rscId, props, rsfId, args }) - - // TODO handle errors - - // pipeable.on('error', (err) => { - // console.info('Cannot render RSC', err) - // res.statusCode = 500 - // if (options.mode === 'development') { - // res.end(String(err)) - // } else { - // res.end() - // } - // }) - pipeable.pipe(res) - return + const handleError = (err: unknown) => { + if (hasStatusCode(err)) { + res.statusCode = err.statusCode + } else { + console.info('Cannot render RSC', err) + res.statusCode = 500 + } + + res.end(String(err)) + // TODO (RSC): When we have `yarn rw dev` support we should do this: + // if (options.command === 'dev') { + // res.end(String(err)) + // } else { + // res.end() + // } + } + + try { + const pipeable = await renderRSC({ rscId, props, rsfId, args }) + // TODO (RSC): See if we can/need to do more error handling here + // pipeable.on(handleError) + pipeable.pipe(res) + } catch (e) { + handleError(e) + } } }) diff --git a/yarn.lock b/yarn.lock index 60a605fe97e7..4949ca4083a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9144,12 +9144,14 @@ __metadata: "@redwoodjs/project-config": 6.0.7 "@redwoodjs/web": 6.0.7 "@swc/core": 1.3.60 + "@types/busboy": ^1 "@types/express": 4 "@types/react": 18.2.14 "@types/yargs-parser": 21.0.0 "@vitejs/plugin-react": 4.0.4 acorn-loose: 8.3.0 buffer: 6.0.3 + busboy: ^1.6.0 core-js: 3.32.2 dotenv-defaults: 5.0.2 express: 4.18.2 @@ -11136,6 +11138,15 @@ __metadata: languageName: node linkType: hard +"@types/busboy@npm:^1": + version: 1.5.0 + resolution: "@types/busboy@npm:1.5.0" + dependencies: + "@types/node": "*" + checksum: 5ba425839c89ba70f504047e4c85068f07309d47c35407645a8b29c2b68bd85bf93b1ca11ed431657c63f602d465b18918f704ed6bf551060ae53ce83d357800 + languageName: node + linkType: hard + "@types/cacheable-request@npm:^6.0.1": version: 6.0.3 resolution: "@types/cacheable-request@npm:6.0.3"