Skip to content

Commit

Permalink
RSC: createRscRequestHandler (redwoodjs#9330)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe authored Oct 22, 2023
1 parent 104c8e2 commit 433d8ef
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 101 deletions.
94 changes: 94 additions & 0 deletions packages/vite/src/rsc/rscRequestHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import busboy from 'busboy'
import type { Request, Response } from 'express'
import RSDWServer from 'react-server-dom-webpack/server.node.unbundled'

import { hasStatusCode } from '../lib/StatusError'
import { renderRSC } from '../waku-lib/rsc-handler-worker'

const { decodeReply, decodeReplyFromBusboy } = RSDWServer

export function createRscRequestHandler() {
// This is mounted at /RSC, so will have /RSC stripped from req.url
return async (req: Request, res: Response) => {
const basePath = '/RSC/'
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
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)
console.log('rsfId', rsfId)

if (rscId && rscId !== '_') {
res.setHeader('Content-Type', 'text/x-component')
props = JSON.parse(params.get('props') || '{}')
} else {
rscId = undefined
}

if (rsfId) {
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 handleError = (err: unknown) => {
if (hasStatusCode(err)) {
res.statusCode = err.statusCode
} else {
console.info('Cannot render RSC', err)
res.statusCode = 500
}

// Getting a warning on GitHub about this
// https://github.com/redwoodjs/redwood/security/code-scanning/211
// Handle according to TODO below
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)
}
}
}
}
108 changes: 7 additions & 101 deletions packages/vite/src/runRscFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,17 @@
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 { createRscRequestHandler } from './rsc/rscRequestHandler'
import { registerFwGlobals } from './streaming/registerGlobals'
import { renderRSC, setClientEntries } from './waku-lib/rsc-handler-worker'

const { decodeReply, decodeReplyFromBusboy } = RSDWServer
import { setClientEntries } from './waku-lib/rsc-handler-worker'

/**
* TODO (STREAMING)
Expand All @@ -39,10 +34,7 @@ loadDotEnv({
defaults: path.join(getPaths().base, '.env.defaults'),
multiline: true,
})
//------------------------------------------------

const checkUaForSeoCrawler = isbot.spawn()
checkUaForSeoCrawler.exclude(['chrome-lighthouse'])
// ------------------------------------------------

export async function runFeServer() {
const app = express()
Expand Down Expand Up @@ -72,10 +64,7 @@ export async function runFeServer() {
// const routeManifest: RWRouteManifest = JSON.parse(routeManifestStr)

// TODO See above about using `import { with: { type: 'json' } }` instead
const manifestPath = path.join(
getPaths().web.dist,
'client-build-manifest.json'
)
const manifestPath = path.join(rwPaths.web.dist, 'client-build-manifest.json')
const buildManifestStr = await fs.readFile(manifestPath, 'utf-8')
const buildManifest: ViteBuildManifest = JSON.parse(buildManifestStr)

Expand All @@ -91,11 +80,11 @@ export async function runFeServer() {
throw new Error('Could not find index.html in build manifest')
}

// 👉 1. Use static handler for assets
// 1. Use static handler for assets
// For CF workers, we'd need an equivalent of this
app.use('/assets', express.static(rwPaths.web.dist + '/assets'))

// 👉 2. Proxy the api server
// 2. Proxy the api server
// TODO (STREAMING) we need to be able to specify whether proxying is required or not
// e.g. deploying to Netlify, we don't need to proxy but configure it in Netlify
// Also be careful of differences between v2 and v3 of the server
Expand All @@ -114,91 +103,8 @@ export async function runFeServer() {
})
)

app.use((req, _res, next) => {
console.log('req.url', req.url)
next()
})

// Mounting middleware at /RSC will strip /RSC from req.url
app.use('/RSC', async (req, res) => {
const basePath = '/RSC/'
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
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)
console.log('rsfId', rsfId)

if (rscId && rscId !== '_') {
res.setHeader('Content-Type', 'text/x-component')
props = JSON.parse(params.get('props') || '{}')
} else {
rscId = undefined
}

if (rsfId) {
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 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)
}
}
})
app.use('/RSC', createRscRequestHandler())

app.use(express.static(rwPaths.web.dist))

Expand Down

0 comments on commit 433d8ef

Please sign in to comment.