Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,17 @@
"author": "Kent C. Dodds <me@kentcdodds.com> (https://kentcdodds.com/)",
"license": "MIT",
"dependencies": {
"body-parser": "^1.20.2",
"busboy": "^1.6.0",
"chalk": "^5.3.0",
"close-with-grace": "^1.3.0",
"compression": "^1.7.4",
"express": "^4.19.1",
"hono": "^4.2.9",
"react": "19.0.0-beta-94eed63c49-20240425",
"react-dom": "19.0.0-beta-94eed63c49-20240425",
"react-error-boundary": "^4.0.13",
"react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.0-beta-94eed63c49-20240425"
},
"devDependencies": {
"@hono/node-server": "^1.11.1",
"@types/node": "^20.11.30",
"prettier": "^3.2.5"
},
Expand Down
85 changes: 54 additions & 31 deletions exercises/05.actions/05.solution.history-revalidation/server/app.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import bodyParser from 'body-parser'
import { Readable } from 'node:stream'
import busboy from 'busboy'
import closeWithGrace from 'close-with-grace'
import compress from 'compression'
import express from 'express'
import { createElement as h } from 'react'
import {
renderToPipeableStream,
Expand All @@ -13,54 +11,73 @@ import { shipDataStorage } from './async-storage.js'

const PORT = process.env.PORT || 3000

const app = express()
import { Hono } from 'hono'
import { compress } from 'hono/compress'
import { serveStatic } from '@hono/node-server/serve-static'
import { serve } from '@hono/node-server'

const app = new Hono()

app.use(compress())
// this is here so the workshop app knows when the server has started
app.head('/', (req, res) => res.status(200).end())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking we're ok without this.

What you had before meant that when the browser requested the root of the app you'd just get an empty response instead of the index.html.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HEAD is generally better if you want to check if the app is running. But a good think about it is that you don't have to have an explicit app.head('/') to handle it. As I said, HEAD is handled by GET as a fallback, but will receive no response body, which is precisely what you want for the HEAD request.

app.get('/', () => new Response())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HEAD requests are handled by GET handlers. This is true for Express as well.


app.use(express.static('public', { index: false }))
app.use('/js/src', express.static('src'))
app.use(serveStatic({ root: 'public', index: false }))
app.use('/js/src', serveStatic({ root: 'src' }))

// This just cleans up the URL if the search ever gets cleared... Not important
// for RSCs... Just ... I just can't help myself. I like URLs clean.
app.use((req, res, next) => {
if (req.query.search === '') {
const searchParams = new URLSearchParams(req.search)
app.use(({ req, redirect }) => {
if (req.query('search') === '') {
/**
* @fixme Not sure what Kent means by
* `req.query.search` and `req.search`.
*/
const searchParams = new URLSearchParams()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only place I didn't figure out. There's some difference between req.search and req.query.search in Express, and I don't know it what that is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find req.search in Express API docs. Hmm.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kentcdodds, was this logic perhaps originally flawed a bit? Shouldn't it be this:

if (req.query.search !== '') {
  // Then remove "search" from query
  // and redirect.
}

searchParams.delete('search')
const location = [req.path, searchParams.toString()]
.filter(Boolean)
.join('?')
return res.redirect(302, location)
} else {
next()
return redirect(location, 302)
}
})

const moduleBasePath = new URL('../src', import.meta.url).href

async function renderApp(res, returnValue) {
/**
*
* @param {import("hono").Context} context
*/
async function renderApp(context, returnValue) {
const { req, res } = context
try {
const shipId = res.req.params.shipId || null
const search = res.req.query.search || ''
const shipId = req.param('shipId') || null
const search = req.query('search') || ''
const data = { shipId, search }
// Since Hono operates with web streams
// and "react-server-dom-esm" with Node.js streams,
// create a Readable, pipe RSD there, and convert it
// to a web ReadableStream for response.
const readable = new Readable()
shipDataStorage.run(data, () => {
const root = h(App)
const payload = { root, returnValue }
const { pipe } = renderToPipeableStream(payload, moduleBasePath)
pipe(res)
pipe(readable)
})
return new Response(Readable.toWeb(readable))
} catch (error) {
console.error(error)
res.status(500).json({ error: error.message })
}
}

app.get('/rsc/:shipId?', async (req, res) => {
await renderApp(res, null)
app.get('/rsc/:shipId?', async context => {
await renderApp(context, null)
})

app.post('/action/:shipId?', bodyParser.text(), async (req, res) => {
const serverReference = req.get('rsc-action')
app.post('/action/:shipId?', async context => {
const serverReference = context.req.header('rsc-action')
const [filepath, name] = serverReference.split('#')
const action = (await import(filepath))[name]
// Validate that this is actually a function we intended to expose and
Expand All @@ -70,24 +87,30 @@ app.post('/action/:shipId?', bodyParser.text(), async (req, res) => {
throw new Error('Invalid action')
}

const bb = busboy({ headers: req.headers })
const bb = busboy({
// Busboy expects Node.js IncomingHeaders, which is an object.
headers: Object.fromEntries(context.req.raw.headers.entries()),
})
const reply = decodeReplyFromBusboy(bb, moduleBasePath)
req.pipe(bb)
Readable.fromWeb(context.req.raw.body).pipe(bb)
const args = await reply
const result = await action(...args)

await renderApp(res, result)
})

app.get('/:shipId?', async (req, res) => {
res.set('Content-type', 'text/html')
return res.sendFile('index.html', { root: 'public' })
})
app.get('/:shipId?', serveStatic({ root: 'public', path: 'index.html' }))

const server = app.listen(PORT, () => {
console.log(`🚀 We have liftoff!`)
console.log(`http://localhost:${PORT}`)
})
const server = serve(
{
port: PORT,
fetch: app.fetch,
},
() => {
console.log(`🚀 We have liftoff!`)
console.log(`http://localhost:${PORT}`)
},
)

closeWithGrace(async ({ signal, err }) => {
if (err) console.error('Shutting down server due to error', err)
Expand Down
22 changes: 19 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.