-
Notifications
You must be signed in to change notification settings - Fork 58
use hono as server #3
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
Changes from 2 commits
267371d
8a9d5d1
cdd8165
52d2ce1
d0a76bf
75753e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
|
|
@@ -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()) | ||
| app.get('/', () => new Response()) | ||
|
||
|
|
||
| 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() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't find There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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' })) | ||
kettanaito marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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) | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.